天天看点

前端基础知识总结(一)

Script标签:向html插入js的方法

属性 描述
async 立即下载脚本(仅适用于外部脚本)。
charset 表示通过src属性指定的代码的字符集
defer 表示脚本可以延迟到文档完全被解析和显示之后再执行(仅适用于外部脚本)。
language script(已废弃) 表示编写代码使用的脚本语言。用 type 属性代替它。
src URL 规定外部脚本文件的 URL。
xml:space preserve 规定是否保留代码中的空白。
type text/xxx language的替换属性,表示编写代码使用的脚本语言的内容类型,也称为MIME属性。

所有<script>元素会按照在页面出现的先后顺序依次被解析,没有 defer 或 async,浏览器会立即加载并执行指定的脚本, 只有解析完前面的script元素的内容后,才会解析后面的代码。

使用<script>的两种方式
  1. 页面中嵌入script代码, 只需指定type属性
<script type="text/javascript">
  // 内部不能出现'</script>'字符串,如果必须出现,必须使用转义标签‘\’
  function sayHi() {
    console.log('hihihi');
    alert('<\/script>');
  }
</script>      

包含在<script>元素内的代码会从上而下依次解释,在解释器对<script>元素内的所有代码求值完毕之前,页面中的其余内容都不会被浏览器加载或显示

  1. 包含外部js文件, src属性是必须的。
<script src="example.js"></script>
// 带有src属性的元素不应该在标签之间包含额外的js代码,即使包含,只会下载并执行外部文件,内部代码也会被忽略。      

与嵌入式js代码一样, 在解析外部js文件时,页面的处理会暂时停止。

改变脚本行为的方法

1. defer: 立即下载,延迟执行

加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后。

会先与DOMContentLoaded事件执行。

延迟脚本总会按照指定他们的顺序执行。

<script defer="defer" src="example.js"></script>      

2. async: 异步脚本

加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

一定会在load事件之前执行,可能会在DOMContentLoaded事件之前或之后执行。

不能保证异步脚本按照他们在页面中出现的顺序执行。

<script async="async" src="example.js"></script>      

区别:

  1. defer 和 async 在网络读取(下载)是一样的,都是异步的(相较于 HTML 解析)
  2. 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
  3. 关于 defer,它是按照加载顺序执行脚本的
  4. async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
  5. async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics

基本类型和引用类型

基本类型:undefined、null、string、number、boolean、symbol

特点

  1. 基本类型的值是不可变得
// 任何方法都无法改变一个基本类型的值
 let name = 'jay';
 name.toUpperCase(); // 输出 'JAY'
 console.log(name); // 输出  'jay'      
  1. 基本类型的比较是值的比较
// 只有在它们的值相等的时候它们才相等
let a = 1;
let b = true;
console.log(a == b); //true
// 用==比较两个不同类型的变量时会进行一些类型转换。
// 先会把true转换为数字1再和数字1进行比较,结果就是true了      
  1. 基本类型的变量是存放在栈区的(栈区指内存里的栈内存)
引用类型:Object、Array、RegExp、Date、Function

也可以说是就是对象。对象是属性和方法的集合,也就是说引用类型可以拥有属性和方法,属性又可以包含基本类型和引用类型

  1. 引用类型的值是可变的
// 我们可为为引用类型添加属性和方法,也可以删除其属性和方法
let person = { name: 'pig' };
person.age = 22;
person.sayName = () => console.log(person.name); 
person.sayName(); // 'pig'
delete person.name;      
  1. 引用类型的比较是引用的比较
let person1 = '{}';
let person2 = '{}';
console.log(person1 == person2); // 字符串值相同,true

let person1 = {};
let person2 = {};
console.log(person1 == person2); // 两个对象的堆内存中的地址不同,false      
  1. 引用类型的值是同时保存在栈内存和堆内存中的对象

javascript和其他语言不同,其不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。实际上,是操作对象的引用,所以引用类型的值是按引用访问的。准确地说,引用类型的存储需要内存的栈区和堆区(堆区是指内存里的堆内存)共同完成,栈区内存保存变量标识符和指向堆内存中该对象的指针,也可以说是该对象在堆内存的地址。

闭包

闭包就是指有权访问另一个函数作用域中的变量的函数。

官方解释:闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。(词法作用域)

通俗解释:闭包的关键在于:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象。

当某个函数被掉用的时候,会创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位...直至作为作用域链终点的全局执行环境。

作用域链本质上是一个指向变量对象的指针列表,他只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相同名字的变量,一般来讲,当函数执行完毕,局部活动对象就会被销毁,内存中仅保存全部作用域的活动对象。但是,闭包不同。

创建闭包: 在一个函数内部创建另一个函数
function add() {
  let a = 1;
  let b = 3;
  function closure() {
     b++;
     return a + b;
  }
  return closure;
}
// 闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。      
生命周期

通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。

当闭包中的函数closure从add中返回后,它的作用域链被初始化为包含add函数的活动对象和全局变量对象。这样closure就可以访问在add中定义的所有变量。更重要的是,add函数在执行完毕后,也不会销毁,因为closure函数的作用域链仍然在引用这个活动对象。换句话说,当add返回后,其执行环境的作用域链被销毁,但它的活动对象仍然在内存中,直至closure被销毁。

function add(x) {
  function closure(y) {
     return x + y;
  }
  return closure;
}

let add2 = add(2);
let add5 = add(5);
// add2 和 add5 共享相同的函数定义,但是保存了不同的环境
// 在add2的环境中,x为5。而在add5中,x则为10
console.log(add2(3)); // 5
console.log(add5(10)); // 15

// 释放闭包的引用
add2 = null;
add5 = null;      
闭包中的this对象
var name = 'window';
var obj = {
  name: 'object',
  getName: () => {
    return () => {
      return this.name;
    }
  }
}
console.log(obj.getName()()); // window      

obj.getName()()是在全局作用域中调用了匿名函数,this指向了window。

函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this就指向哪里。

window才是匿名函数功能执行的环境。

使用注意点

1)由于闭包会让包含函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

使用
  1. 模仿块级作用域
  2. 私有变量
  3. 模块模式
在循环中创建闭包:一个常见错误
function show(i) {
  console.log(i);
}

function showCallback(i) {
  return () => {
    show(i);
  };
}

// 测试1【3,3,3】
const testFunc1 = () => {
  // var i;
  for (var i = 0; i < 3; i++) {
    setTimeout(() => show(i), 300);
  }
}

// 测试2 【0,1,2】
const testFunc2 = () => {
  for (var i = 0; i < 3; i++) {
    setTimeout(showCallback(i), 300);
  }
}

// 测试3【0,1, 2】 闭包,立即执行函数
// 在闭包函数内部形成了局部作用域,每循环一次,形成一个自己的局部作用域
const testFunc3 = () => {
  for (var i = 0; i < 3; i++) {
    (() => {
       setTimeout(() => show(i), 300);
    })(i);
  }
}

// 测试4【0,1, 2】let
const testFunc4 = () => {
  for (let i = 0; i < 3; i++) {
    setTimeout(() => show(i), 300);
  }
}      

setTimeout()函数回调属于异步任务,会出现在宏任务队列中,被压到了任务队列的最后,在这段代码应该是for循环这个同步任务执行完成后才会轮到它

测试1错误原因:赋值给 setTimeout 的是闭包。这些闭包是由他们的函数定义和在 testFunc1 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量i。这是因为变量i使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,i的值被决定。由于循环在事件触发之前早已执行完毕,变量对象i(被三个闭包所共享)已经指向了i的最后一个值。

测试2正确原因: 所有的回调不再共享同一个环境, showCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,i 指向数组中对应的下标。

测试4正确原因:JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。

作用域

javascript中变量或函数产生作用、而不会对外产生影响的封闭空间。外部不可以访问内部变量或函数,但内部能够访问外部。

ES5:

  1. 全局作用域:所有地方都可以访问
  2. 函数作用域:只能在函数内部访问

ES6:

  1. 增加了块级作用域(最近大括号的作用范围),但仅限于let声明的变量
作用域链

规则:

  1. 全局变量,函数申明都是属于0级链,每个对象占一个位置
  2. 凡是看到函数就延伸一个链出来,一级级展开
  3. 访问首先看到当前函数,如果当前作用域链没有定义,往上级链中检查
  4. 如此往复,直到0级链,如果0级没有,则弹出错误,这个变量没有定义
词法作用域

所谓词法(代码)作用域,就是代码在编写过程中体现出来的作用范围,代码一旦写好了,没有运行之前(不用执行),作用范围就已经确定好了,这个就是所谓的词法作用域。

词法作用域的规则:

  1. 函数允许访问函数外部的数据
  2. 整个代码结构中只有函数才能限定作用域
  3. 作用规则首先使用变量提升规则分析
  4. 如果当前作用规则里面有该名字,则不考虑外面的外面的名字

词法作用域根据声明变量的位置来确定该变量可被访问的位置。嵌套函数可获取声明于外部作用域的函数。

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();      
var let 区别
  1. var声明的变量,只有函数才能为它创建新的作用域

let支持块级作用域,花括号就能为它创建新的作用域

  1. 相同作用域,var可以反复声明相同标识符的变量,而let是不允许的;
  2. let声明的变量禁止在声明前访问
// 全局变量
var i = 0 ;
// 定义外部函数
function outer(){
    // 访问全局变量
    console.log(i); // 0
  
    function inner1(){
        console.log(i); // 0
    }

    function inner2(){
        console.log(i); // undefined
        var i = 1;
        console.log(i); // 1
    }
  
    inner1();
    inner2();
    console.log(i); // 0
}      

变量提升

在Javascript中,函数及变量的声明都将被提升到函数的最顶部,提升的仅仅是变量的声明,变量的赋值并不会被提升。我们需要注意的是,函数的声明与变量的声明是不一样的。函数的函数体也会被一起提升。

函数表达式和变量表达式只是其声明被提升,函数声明是函数的声明和实现都被提升。

function foo() {  
  console.log("global foo");  
}  

function bar() {  
   console.log("global bar");  
}  

//定义全局变量  
var v = "global var";  

function hoistMe() {
   // var bar; 被提升到顶部,并未实现
   // var v;
   console.log(typeof foo); //function  
   console.log(typeof bar); //undefined  
   console.log(v); //undefined  
  
   // 函数里面定义了同名的函数和变量,无论在函数的任何位置定义这些函数和和变量,它们都将被提升到函数的最顶部。  

   foo(); //local foo  
   bar(); //报错,TypeError "bar is not a function"

    //函数声明,变量foo以及其实现被提升到hoistMe函数顶部  
   function foo() {  
     alert("local foo");  
   }  

   //函数表达式,仅变量bar被提升到函数顶部,实现没有被提升  
   var bar = function() {  
       alert("local bar");  
   };  

   //定义局部变量  
   var v = "local";  
}      

let 变量提升

console.log(a); // Uncaught ReferenceError: a is not defined
let a = "I am a";

let b = "I am outside B";
if(true){
    console.log(b); // Uncaught ReferenceError: b is not defined
    let b = " I am inside B";
}      

如果b没有变量提升,执行到console.log时应该是输出全局作用域中的b,而不是出现错误。

我们可以推知,这里确实出现了变量提升,而我们不能够访问的原因事实上是因为let的死区设计:当前作用域顶部到该变量声明位置中间的部分,都是该let变量的死区,在死区中,禁止访问该变量。由此,我们给出结论,let声明的变量存在变量提升, 但是由于死区我们无法在声明前访问这个变量。

this 指向问题

this 就是一个指针,指向我们调用函数的对象。

执行上下文:  是语言规范中的一个概念,用通俗的话讲,大致等同于函数的执行“环境”。具体的有:变量作用域(和 作用域链条,闭包里面来自外部作用域的变量),函数参数,以及 this 对象的值。

找出 this 的指向

this 的值并不是由函数定义放在哪个对象里面决定,而是函数执行时由谁来唤起决定。

var name = "Jay Global";
var person = {
    name: 'Jay Person',
    details: {
        name: 'Jay Details',
        print: function() {
            return this.name;
        }
    },
    print: function() {
        return this.name;
    }
};

console.log(person.details.print());  // 【details对象调用的print】Jay Details
console.log(person.print());          // 【person对象调用的print】Jay Person

var name1 = person.print;
var name2 = person.details;

console.log(name1()); // 【name1前面没有调用对象,所以是window】Jay Global
console.log(name2.print()) // 【name2对象调用的print】Jay Details      
this和箭头函数

箭头函数按词法作用域来绑定它的上下文,所以 this 实际上会引用到原来的上下文。

箭头函数保持它当前执行上下文的词法作用域不变,而普通函数则不会。换句话说,箭头函数从包含它的词法作用域中继承到了 this 的值。

匿名函数,它不会作为某个对象的方法被调用,  因此,this 关键词指向了全局 window 对象。

var object = {
    data: [1,2,3],
    dataDouble: [1,2,3],
    double: function() {
        console.log(this); // object
        return this.data.map(function(item) { // this是当前object,object调用的double
            console.log(this);   // 传给map()的那个匿名函数没有被任一对象调用,所以是window
            return item * 2;
        });
    },
    doubleArrow: function() {
        console.log(this); // object
        return this.dataDouble.map(item => { // this是当前object,object调用的doubleArrow
            console.log(this);      // doubleArrow是object调用的,这就是上下文,所以是window
            return item * 2;
        });
    }
};
object.double();
object.doubleArrow();      
明确设置执行上下文

在 JavaScript 中通过使用内置的特性开发者就可以直接操作执行上下文了。这些特性包括:

  • bind():不需要执行函数就可以将 this 的值准确设置到你选择的一个对象上。通过逗号隔开传递多个参数。 设置好 this 关键词后不会立刻执行函数。
  • apply():将 this 的值准确设置到你选择的一个对象上。apply(thisObj, argArray)接收两个参数,thisObj是函数运行的作用域(this),argArray是参数数组,数组的每一项是你希望传递给函数的参数。如果没有提供argArray和thisObj任何一个参数,那么Global对象将用作thisObj。最后,会立刻执行函数。
  • call():将 this 的值准确设置到你选择的一个对象上。然后像bind 一样通过逗号分隔传递多个参数给函数。语法:call(thisObj,arg1,arg2,..., argn);,如果没有提供thisObj参数,那么Global对象被用于thisObj。最后,会立刻执行函数。

this 和 bind

var bobObj = {
    name: "Bob"
};
function print() {
    return this.name;
}
var printNameBob = print.bind(bobObj);
console.log(printNameBob()); // Bob      

this 和 call

function add(a, b) { 
    return a + b; 
}
function sum() {
    return Array.prototype.reduce.call(arguments, add);
}
console.log(sum(1,2,3,4)); // 10      

this 和 apply

apply 就是接受数组版本的call。

Math.min(1,2,3,4); // 返回 1
Math.min([1,2,3,4]); // 返回 NaN。只接受数字
Math.min.apply(null, [1,2,3,4]); // 返回 1

function Person(name, age){  
  this.name = name;  
  this.age = age;  
}  

function Student(name, age, grade) {  
  Person.apply(this, arguments);  //Person.call(this, name, age);
  this.grade = grade;  
}
var student = new Student("sansan", 21, "一年级");  
 
console.log("student:", student); // {name: 'sansan'; age: '21', grade: '一年级'}      

如果你的参数本来就存在一个数组中,那自然就用 apply,如果参数比较散乱相互之间没什么关联,就用 call。

对象属性类型

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值,数据属性有4个描述其行为的特性:

  • Configurable: 表示是否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值是true
  • Enumerable: 表示能否通过for-in循环返回属性。默认值是true
  • Writable: 表述能否修改属性。默认值是true
  • Value: 包含这个属性的数据值。默认值是true
访问器属性

函数式编程

函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变。

纯函数

纯函数是稳定的、一致的和可预测的。给定相同的参数,纯函数总是返回相同的结果。

纯函数有一个非常严格的定义:

  • 如果给定相同的参数,则返回相同的结果(也称为确定性)。
  • 它不会引起任何副作用。

纯函数特性

  1. 如果给定相同的参数,则得到相同的结果

我们想要实现一个计算圆的面积的函数。

不是纯函数会这样做:

  let PI = 3.14;    
  const calculateArea = (radius) => radius * radius * PI; 
  // 它使用了一个没有作为参数传递给函数的全局对象
  calculateArea(10); // returns 314.0      

纯函数:

 let PI = 3.14;    
 const calculateArea = (radius, pi) => radius * radius * pi; 
 // 现在把 PI 的值作为参数传递给函数,这样就没有外部对象引入。
 calculateArea(10, PI); // returns 314.0      
  1. 无明显副作用

纯函数不会引起任何可观察到的副作用。可见副作用的例子包括修改全局对象或通过引用传递的参数。

现在,实现一个函数,接收一个整数并返对该整数进行加1操作且返回。

let counter = 1;
function increaseCounter(value) {      
  counter = value + 1;   
}    
increaseCounter(counter);   
console.log(counter); // 2      

该非纯函数接收该值并重新分配counter,使其值增加1。

函数式编程不鼓励可变性(修改全局对象)。

 let counter = 1;
 const increaseCounter = (value) => value + 1;   // 函数返回递增的值,而不改变变量的值
 increaseCounter(counter); // 2    
 console.log(counter); // 1      

纯函数的好处

纯函数代码肯定更容易测试,不需要 mock 任何东西,因此,我们可以使用不同的上下文对纯函数进行单元测试:

一个简单的例子是接收一组数字,并对每个数进行加 1

let list = [1, 2, 3, 4, 5];    
const incrementNumbers = (list) => list.map(number => number + 1);
incrementNumbers(list); // [2, 3, 4, 5, 6]      

对于输入[1,2,3,4,5],预期输出是[2,3,4,5,6]。

引用透明性

实现一个square 函数:

const square = (n) => n * n;
square(2); // 4 将2作为square函数的参数传递始终会返回4      

可以把square(2)换成4,我们的函数就是引用透明的。

如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。

函数也可以被看作成值并用作数据使用。

  • 从常量和变量中引用它。
  • 将其作为参数传递给其他函数。
  • 作为其他函数的结果返回它。

其思想是将函数视为值,并将函数作为数据传递。通过这种方式,我们可以组合不同的函数来创建具有新行为的新函数。

假如我们有一个函数,它对两个值求和,然后将值加倍,如下所示:

 const doubleSum = (a, b) => (a + b) * 2;      

对应两个值求差,然后将值加倍:

  const doubleSubtraction = (a, b) => (a - b) * 2      

这些函数具有相似的逻辑,但区别在于运算符的功能。如果我们可以将函数视为值并将它们作为参数传递,我们可以构建一个接收运算符函数并在函数内部使用它的函数。

 const sum = (a, b) => a + b;    
 const subtraction = (a, b) => a - b;    
 const doubleOperator = (f, a, b) => f(a, b) * 2;    
 doubleOperator(sum, 3, 1); // 8    
 doubleOperator(subtraction, 3, 1); // 4      

Promise

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。

基本过程:

  1. 初始化 Promise 状态(pending)
  2. 执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  3. 立即执行 Promise 中传入的 fn 函数,将Promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理
  4. Promise中要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行。

真正的链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise.

跨域

不同地址,不同端口,不同级别,不同协议都会构成跨域。

跨域方案

window.postMessage

window.postMessage是html5的功能,是客户端和客户端直接的数据传递,既可以跨域传递,也可以同域传递。

栗子:假如有一个页面,页面中拿到部分用户信息,点击进入另外一个页面,另外的页面默认是取不到用户信息的,你可以通过window.postMessage把部分用户信息传到这个页面中。(需要考虑安全性等方面。)

发送信息

//弹出一个新窗口
var domain = 'http://haorooms.com';
var myPopup = window.open(`${domain}/windowPostMessageListener.html`,'myWindow');

//周期性的发送消息
setTimeout(function(){
    var message = {name:"站点",sex:"男"}; //你在这里也可以传递一些数据,obj等
    console.log('传递的数据是  ' + message);
    myPopup.postMessage(message, domain);
},1000);      

要延迟一下,我们一般用计时器setTimeout延迟再发用。

接受的页面

//监听消息反馈
window.addEventListener('message',function(event) {
    if(event.origin !== 'http://haorooms.com') return;
    //这个判断一下是不是我这个域名跳转过来的
    console.log('received response: ', event.data);
}, false);      

如下图,接受页面得到数据

前端基础知识总结(一)

如果是使用iframe,代码应该这样写:

//捕获iframe
var domain = 'http://haorooms.com';
var iframe = document.getElementById('myIFrame').contentWindow;

//发送消息
setTimeout(function(){ 
    var message = {name:"站点",sex:"男"};
    console.log('传递的数据是:  ' + message);
    iframe.postMessage(message, domain); 
},1000);      

接受数据

//响应事件
window.addEventListener('message',function(event) {
    if(event.origin !== 'http://haorooms.com') return;
    console.log('message received:  ' + event.data,event);
    event.source.postMessage('holla back youngin!',event.origin);
},false);      

上面的代码片段是往消息源反馈信息,确认消息已经收到。下面是几个比较重要的事件属性:

**source – 消息源,消息的发送窗口/iframe。

origin – 消息源的URI(可能包含协议、域名和端口),用来验证数据源。

data – 发送方发送给接收方的数据。**

CORS跨域资源共享

CORS跨域和jsonp跨域的优势:

CORS与JSONP相比,无疑更为先进、方便和可靠。

1、 JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。

2、 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。

3、 JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS

CORS的使用

CORS要前后端同时做配置。

1、首先我们来看前端。

纯js的ajax请求。

<script type="text/javascript">
    var xhr = new XMLHttpRequest(); 
    // ie6以下用new ActiveXObject("Microsoft.XMLHTTP");可以做能力判断。
    xhr.open("GET", "/haorooms", true);
    xhr.send();
</script>      

以上的haorooms是相对路径,如果我们要使用CORS,相关Ajax代码可能如下所示:

<script type="text/javascript">
    var xhr = new XMLHttpRequest();
    //ie6以下用new ActiveXObject("Microsoft.XMLHTTP");可以做能力判断。
    xhr.open("GET", "http://www.haorooms.com/CORS", true);
    xhr.send();
</script>      

当然,你也可以用jquery的ajax进行。

2、后端或者服务器端的配置

下面我们主要介绍Apache和PHP里的设置方法。

Apache:Apache需要使用mod_headers模块来激活HTTP头的设置,它默认是激活的。你只需要在Apache配置文件的 < Directory >, < Location>, < Files >或< VirtualHost>的配置里加入以下内容即可:

Header set Access-Control-Allow-Origin *      

PHP:只需要使用如下的代码设置即可。

<?php  
 header("Access-Control-Allow-Origin:*");      

以上的配置的含义是允许任何域发起的请求都可以获取当前服务器的数据。当然,这样有很大的危险性,恶意站点可能通过XSS攻击我们的服务器。所以我们应该尽量有针对性的对限制安全的来源,例如下面的设置使得只有www.haorooms.com这个域才能跨域访问服务器的API。

 Access-Control-Allow-Origin: http://www.haorooms.com      
通过jsonp跨域

jsonp跨域也需要前后端配合使用。一般后端设置callback ,前端给后台接口中传一个callback 就可以。

例如前端代码:

<script type="text/javascript">
    function dosomething(jsondata){
        //处理获得的json数据
    }
</script>
<script src="http://haorooms.com/data.php?callback=dosomething"></script>      

后台代码:

<?php
  $callback = $_GET['callback'];//得到回调函数名
  $data = array('a','b','c');//要返回的数据
  echo $callback.'('.json_encode($data).')';//输出
?>      

假如你用ajax方式进行jsonp跨域:

$.ajax({  
    type : "get",  
    url : "跨域地址",  
    dataType : "jsonp",//数据类型为jsonp  
    jsonp: "callback",
    //服务端用于接收callback调用的function名的参数【后台接受什么参数,我们就传什么参数】
    success : function(data){  
        //结果处理
    },  
    error:function(data){  
          console.log(data);
    }  
});      
通过修改document.domain来跨子域

我们只需要在跨域的两个页面中设置document.domain就可以了。修改document.domain的方法只适用于不同子域的框架间的交互。

栗子:

1.在页面 http:// www.haorooms.com/a.html 中设置document.domain

<iframe id = "iframe" src="http://haorooms.com/b.html" onload = "test()"></iframe>
<script type="text/javascript">
    document.domain = 'haorooms.com';//设置成主域
    function test(){
       alert(document.getElementById('iframe').contentWindow);
       //contentWindow 可取得子窗口的 window 对象
    }
</script>      

2、在页面http:// haorooms.com/b.html 中设置document.domain

<script type="text/javascript">
    document.domain = 'haorooms.com';
    //在iframe载入这个页面也设置document.domain,使之与主页面的document.domain相同
</script>      
使用window.name来进行跨域

原理:

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。

方法:

假如有三个页面。

a.com/app.html:应用页面。
a.com/proxy.html:代理文件,一般是一个没有任何内容的html文件,需要和应用页面在同一域下。
b.com/data.html:应用页面需要获取数据的页面,可称为数据页面。      

1、在应用页面(a.com/app.html)中创建一个iframe,把其src指向数据页面(b.com/data.html)。

数据页面会把数据附加到这个iframe的window.name上,data.html代码如下:

<script type="text/javascript">
   window.name = 'I was there!';    
   // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右
   // 数据格式可以自定义,如json、字符串
</script>      

2、在应用页面(a.com/app.html)中监听iframe的onload事件,在此事件中设置这个iframe的src指向本地域的代理文件(代理文件和应用页面在同一域下,所以可以相互通信)。

app.html部分代码如下:

<script type="text/javascript">
    var state = 0, 
    iframe = document.createElement('iframe'),
    loadfn = function() {
        if (state === 1) {
            var data = iframe.contentWindow.name; // 读取数据
            alert(data);
        } else if (state === 0) {
            state = 1;
            iframe.contentWindow.location = "http://a.com/proxy.html";    
            // 设置的代理文件
        }  
    };
    iframe.src = 'http://b.com/data.html';
    if (iframe.attachEvent) {
        iframe.attachEvent('onload', loadfn);
    } else {
        iframe.onload  = loadfn;
    }
    document.body.appendChild(iframe);
</script>      

3、获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)。

<script type="text/javascript">
    iframe.contentWindow.document.write('');
    iframe.contentWindow.close();
    document.body.removeChild(iframe);
</script>      
webpack解决跨域问题

webpack也可以解决前端跨域问题,只需要安装webpack 的http-proxy-middleware模块就可以

npm install http-proxy-middleware --save-dev      

配置如下:

module.exports = {
  devtool: 'cheap-module-source-map',
  entry: './app/js/index.js'
  output: {
  path: path.resolve(__dirname, 'dev'),
    // 所有输出文件的目标路径
    filename: 'js/bundle.js',
    publicPath: '/',
    chunkFilename: '[name].chunk.js'
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dev'),
    publicPath: '/',
    historyApiFallback: true,
    proxy: {
      // 请求到 '/device' 下的请求都会被代理到target:http://debug.haorooms.com中
      '/device/*': {
        target: 'http://debug.haorooms.com',
        secure: false, // 接受运行在https上的服务
        changeOrigin: true
      }
    }
  }
}      

使用如下:

fetch('/device/space').then(res => {
  // 被代理到 http://debug.haorooms.com/device/space
  return res.json();
});

fetch('device/space').then(res => {
  // http://localhost:8080/device/space 访问本地服务
  return res.json();
});

// 注:使用的url 必须以/开始 否则不会代理到指定地址      
cookie跨域

业务场景

1、百度www域名下面登录了,发现yun域名下面也自然而然登录了。

2、淘宝登录了,发现天猫也登录了,淘宝和天猫是完全不一样的2个域名。

栗子

  1. 同一个主域下面的跨域问题,类似www.baidu 和yun.baidu

cookie属性:

path

cookie 一般都是由于用户访问页面而被创建的,但是并不是只有在创建 cookie 的页面才可以访问这个cookie。在默认情况下,出于安全方面的考虑,只有与创建 cookie 的页面处于同一个目录或在创建cookie页面的子目录下的网页才可以访问。那么此时如果希望其父级或者整个网页都能够使用cookie,就需要进行路径的设置。

path表示cookie所在的目录,haorooms.com默认为/,就是根目录。 在同一个服务器上有目录如下:

/post/,/post/id/,/tag/,/tag/haorooms/,/tag/id/      

现假设一个

cookie1的path为/tag/,

cookie2的path为/tag/id/,

那么tag下的所有页面都可以访问到cookie1,而/tag/和/tag/haorooms/的子页面不能访问cookie2。 这是因为cookie2能让其path路径下的页面访问。

让这个设置的cookie 能被其他目录或者父级的目录访问的方法:

document.cookie = "name = value; path=/";      

domain

domain表示的是cookie所在的域,默认为请求的地址,

如网址为 http://www.haorooms.com/post/... ,那么domain默认为www.haorooms.com。

而跨域访问,

如域A为love.haorooms.com,域B为resource.haorooms.com,

那么在域A生产一个令域A和域B都能访问的cookie就要将该cookie的domain设置为.haorooms.com;

如果要在域A生产一个令域A不能访问而域B能访问的cookie就要将该cookie的domain设置为resource.haorooms.com。

这样,我们就知道为什么www.百度 和yun.baidu共享cookie,我们只需要设置domain为.baidu.com就可以了【注:点好是必须的】

  1. 天猫和淘宝是如何共享cookie的?

cookie跨域解决方案一般有如下几种:

1、nginx反向代理

反向代理(Reverse Proxy)方式是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。

反向代理服务器对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理 的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容 原本就是它自己的一样。

nginx配置如下:

upstream web1{
     server  127.0.0.1:8089  max_fails=0 weight=1;
}
upstream web2 {
     server 127.0.0.1:8080    max_fails=0 weight=1;
}

    location /web1 {
        proxy_pass http://web1;
        proxy_set_header Host  127.0.0.1;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        proxy_set_header Cookie $http_cookie;
        log_subrequest on;
    }

    location /web2 {
        proxy_pass http://web2;
        proxy_set_header Host  127.0.0.1;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header Cookie $http_cookie;
        log_subrequest on;
    }      

3、nodejs superagent

package.json中的模块依赖:

前端基础知识总结(一)

调用superagent api请求:

前端基础知识总结(一)

其实本质也是jsonp的方式。

同一域下,不同工程下的cookie携带问题

cookie跨域访问之后,可以成功的写入本地域。本地的前端工程在请求后端工程时,有很多是ajax请求,ajax默认不支持携带cookie,所以现在有以下两种方案:

(1). 使用jsonp格式发送
(2). ajax请求中加上字段 xhrFields: {withCredentials: true},这样可以携带上cookie      
前端基础知识总结(一)

服务器需要配置:

Access-Control-Allow-Credentials: true      
localStorage跨域

模块化

AMD、CMD 和 CommonJS 的模块化开发

AMD/CMD/CommonJs都是JS模块化开发的标准,目前对应的实现是RequireJS,SeaJs, nodeJs;

CommonJS:服务端js

CommonJS 是以在浏览器环境之外构建 javaScript 生态系统为目标而产生的写一套规范,主要是为了解决 javaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。

实现方法:模块必须通过 module.exports 导出对外的变量或者接口,通过 require() 来导入其他模块的输出到当前模块的作用域中;

主要针对服务端(同步加载文件)和桌面环境中,node.js 遵循的是 CommonJS 的规范;

CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作.

**require()用来引入外部模块;

exports对象用于导出当前模块的方法或变量,唯一的导出口;

module对象就代表模块本身。**

// 定义一个module.js文件
var A = () => console.log('我是定义的模块');

// 1.第一种返回方式
module.exports = A; 
// 2.第二种返回方式 
module.exports.test = A
// 3.第三种返回方式 
exports.test = A;


// 定义一个test.js文件【这两个文件在同一个目录下】
var module = require("./module");

//调用这个模块,不同的返回方式用不同的方式调用
// 1.第一种调用方式
module();
// 2.第二种调用方式 
module.test();
// 3.第三种调用方式 
module.test();


// 执行文件
node test.js      

AMD: 异步模块定义【浏览器端js】

AMD 是 Asynchronous Module Definition 的缩写,意思是异步模块定义;采用的是异步的方式进行模块的加载,在加载模块的时候不影响后边语句的运行。主要是为前端 js 的表现指定的一套规范。

实现方法:通过define方法去定义模块,通过require方法去加载模块。

define(id?,dependencies?,factory): 它要在声明模块的时候制定所有的依赖(dep),并且还要当做形参传到factory中。没什么依赖,就定义简单的模块(或者叫独立的模块)

require([modules], callback):第一个参数[modules],是需加载的模块名数组;第二个参数callback,是模块加载成功之后的回调函数

主要针对浏览器js,requireJs遵循的是 AMD 的规范;

// module1.js文件, 定义独立的模块
define({
    methodA: () => console.log('我是module1的methodA');
    methodB: () => console.log('我是module1的methodB');
});

// module2.js文件, 另一种定义独立模块的方式
define(() => {
    return {
        methodA: () => console.log('我是module2的methodA');
        methodB: () => console.log('我是module2的methodB');
    };
});

// module3.js文件, 定义非独立的模块(这个模块依赖其他模块)
define(['module1', 'module2'], (m1, m2) => {
    return {
        methodC: () => {
            m1.methodA();
            m2.methodB();
        }
    };
});


//定义一个main.js,去加载这些个模块
require(['module3'], (m3) => {
    m3.methodC();
});


// 为避免造成网页失去响应,解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:
<script src="js/require.js" defer async="true" ></script>
// async属性表明这个文件需要异步加载,避免网页失去响应。
// IE不支持这个属性,只支持defer,所以把defer也写上。

// data-main属性: 指定网页程序的主模块
<script data-main="main" src="js/require.js"></script>

// 控制台输出结果
我是module1的methodA
我是module2的methodB      

CMD: 通用模块定义【浏览器端js】

  CMD 是 Common Module Definition 的缩写,通过异步的方式进行模块的加载的,在加载的时候会把模块变为字符串解析一遍才知道依赖了哪个模块;

主要针对浏览器端(异步加载文件),按需加载文件。对应的实现是seajs

AMD和CMD的区别:

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。

  2. CMD 推崇依赖就近,AMD 推崇依赖前置。

// CMD
define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();
    // ...
    var b = require('./b');   // 依赖可以就近书写
    b.doSomething();
    // ... 
})

// AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    // ...
    b.doSomething();
    //...
})      
import和require

import和require都是被模块化使用

  • require是CommonJs的语法(AMD规范引入方式),CommonJs的模块是对象。
  • import是es6的一个语法标准(浏览器不支持,本质是使用node中的babel将es6转码为es5再执行,import会被转码为require),es6模块不是对象
  • require是运行时加载整个模块(即模块中所有方法),生成一个对象,再从对象上读取它的方法(只有运行时才能得到这个对象,不能在编译时做到静态化),理论上可以用在代码的任何地方。
  • import是编译时调用,确定模块的依赖关系,输入变量(es6模块不是对象,而是通过export命令指定输出代码,再通过import输入,只加载import中导的方法,其他方法不加载),import具有提升效果,会提升到模块的头部(编译时执行)

export和import可以位于模块中的任何位置,但是必须是在模块顶层,如果在其他作用域内,会报错

    es6这样的设计可以提高编译器效率,但没法实现运行时加载

  • require是赋值过程,把require的结果(对象,数字,函数等),默认是export的一个对象,赋给某个变量(复制或浅拷贝)
  • import是解构过程(需要谁,加载谁)
// require/exports(仅有下面的三种简单写法)

const a = require('a') //真正被require出来的是来自module.exports指向的内存块内容

exports.a = a //exports 只是 module.exports的引用,辅助module.exports操作内存中的数据
module.exports = a

// import/export
import a from 'a'
import { default as a  } from 'a'
import  *  as a  from 'a'
import { fun1,fun2 } from 'a'

export default a
export const a=1
export functon a{ }
export { fun1,fun2 }      
Require.js

require.js的诞生,就是为了解决这两个问题:

(1)实现js文件的异步加载,避免网页失去响应;

(2)管理模块之间的依赖性,便于代码的编写和维护。

加载js文件

加载require.js,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:

<script src="js/require.js" defer async="true" ></script>      

async属性表明这个文件需要异步加载,避免网页失去响应。

IE不支持async,只支持defer,所以把defer也写上。

require.config()

在用require()加载之前,要先用require.config()方法,定义它们的一些特征

require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",
    "underscore": "underscore.min",
    "backbone": "backbone.min"
  },
  shim: {
    'underscore':{
      exports: '_'
    },
    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    }
  }
});      

从 URL 输入到页面展现大致流程概述:

  1. 在浏览器输入 URL;
  2. 浏览器查找域名对应的 IP 地址;
  3. 浏览器根据 IP 地址与服务器建立联系;
  4. 浏览器与服务器通信:浏览器请求,服务器处理请求并呈现页面。
具体流程概述

第一步,在浏览器输入 URL

在地址栏输入相应的 URL 。

第二步,浏览器查找域名对应的 IP 地址

第一步中,我们已经输入了相应的 URL,但浏览器本身并不能识别 URL 是什么,因此从我们输入 URL 开始,浏览器就要进行域名解析来找到对应 IP——DNS 解析是浏览器的实际寻址方式:

  • 查找浏览器缓存——近期浏览过的网站,浏览器会缓存 DNS 记录一段时间 (如果没有则往下) ;
  • 查找系统缓存——从 C 盘的 hosts 文件查找是否有存储的 DNS 信息,查找是否有目标域名和对应的 IP 地址 (如果没有则往下);
  • 查找路由器缓存 (如果没有则往下);
  • 查找 ISP DNS 缓存——从网络服务商(比如电信)的 DNS 缓存信息中查找(如果没有则往下);
  • 经由以上方式都没找到对应 IP 的话,就会向根域名服务器查找目标 URL 对应的 IP,根域名服务器会向下级服务器转达请求,层层下发,直到找到对应的 IP 为止。

第三步,浏览器根据 IP 地址与服务器建立联系

根据IP建立TCP连接(三次握手)

第 2 步中,浏览器通过 IP 寻址找到了对应的服务器,浏览器就将用户发起的 HTTP 请求发送给服务器。

服务器开始处理用户请求:

  • 每台服务器上都会安装处理请求的应用——Web Server;
  • 常见的 Web Server 产品有:Apache 、Nginx 、IIS 和 lighttpd 等;
  • Web Server 可以理解为一个管理者,它不做具体的请求处理,而是会结合配置文件,把不同用户发来的请求委托给服务器上专门处理相应请求的程序(服务器上的相应程序开始处理请求的这一部分,通俗说就是实际后台处理的工作):

后台开发现在有很多框架,但大部分都是按照 MVC(Model View Controller)设计模式搭建的,它将服务器上的应用程序分成 3 个核心部件且分别处理自己的任务,实现输入、处理、输出的分离:

  • 模型(Model)

模型,是将实际开发过程中的业务规则和所涉及的数据格式进行模型化;

应用于模型的代码只需写一次就可以被多个视图重用;

在 MVC 三个部件中,模型拥有最多的处理任务;

一个模型能为多个视图提供数据。

  • 视图(View)

视图是用户看到并与之交互的界面。

  • 控制器(Controller)

作用:接受用户的输入并调用模型(M)和视图(V)去完成用户的需求;

地位:控制器也是处于一个管理者的地位——从视图(V)接收请求并决定调用哪一个模型构件(M)来处理请求,然后再确定用哪个视图(V)来显示模型(M)处理返回的数据。

总而言之:

首先,控制器(C)接收用户的请求,并决定应该调用哪个模型(M)来进行处理;

然后,模型(M)用业务逻辑来处理用户的请求并返回数据;

最后,控制器(C)再用相应的视图(V)来格式化模型(M),进而返回 HTML 字符串给浏览器。

第四步,浏览器与服务器通信

在上边的第 3 步中,服务器返回了 HTML 字符串给浏览器,此时,浏览器将会对其进行解析、渲染并最终绘制网页:

  1. 加载
  • 浏览器对一个 HTML 页面的加载顺序是从上而下的;
  • 浏览器在加载的过程中,同时进行解析、渲染处理;
  • 在这个过程中,遇到 link 标签、image 标签、script 标签时,浏览器会再次向服务器发送请求以获取相应的 CSS 文件、图片资源、JS 文件,并执行 JS 代码,同步进行加载、解析。
  1. 解析、渲染
  • 解析的过程,其实就是生成“树”(Document Object Model 文档对象模型);
  • DOM 树是由 DOM 元素及属性节点组成,并且加上 CSS 解析的样式对象和 JS 解析后的动作实现;
  • 渲染:就是将 DOM 树进行可视化表示。
  1. 绘制网页
  • 浏览器通过渲染,将 DOM 树可视化,得到渲染树;
  • 构建渲染树使页面以正确的顺序绘制出来,浏览器遵循一定的渲染规则,实现网站页面的绘制,并最终完成页面的展示。

第五步:关闭TCP连接(四次挥手)

http和https的区别

Http:超文本传输协议(Http,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。设计Http最初的目的是为了提供一种发布和接收HTML页面的方法。它可以使浏览器更加高效。Http协议是以明文方式发送信息的,如果黑客截取了Web浏览器和服务器之间的传输报文,就可以直接获得其中的信息。

Https:是以安全为目标的Http通道,是Http的安全版。Https的安全基础是SSL。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol),它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol),它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

HTTP与HTTPS的区别

1、HTTP是超文本传输协议,信息是明文传输,HTTPS是具有安全性的SSL加密传输协议。

2、HTTPS协议需要ca申请证书,一般免费证书少,因而需要一定费用。

3、HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样。前者是80,后者是443。

4、HTTP连接是无状态的,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,安全性高于HTTP协议。

https的优点

尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处:

1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;

2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。

3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。

4)谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。

Https的缺点

1)Https协议握手阶段比较费时,会使页面的加载时间延长近。

2)Https连接缓存不如Http高效,会增加数据开销,甚至已有的安全措施也会因此而受到影响;

3)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。

4)Https协议的加密范围也比较有限。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。

for 循环

for

在for循环中,循环取得数组或是数组类似对象的值,譬如arguments和HTMLCollection对象。

不足:

  • 在于每次循环的时候数组的长度都要去获取;
  • 终止条件要明确;
foreach() map()

两个方法都可以遍历到数组的每个元素,而且参数一致;

不同点:

forEach() : 对数组的每个元素执行一次提供的函数, 总是返回undefined;

map() : 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。 返回值是一个新的数组;

var array1 = [1,2,3,4,5];
 
var x = array1.forEach((value,index) => {
    console.log(value);
    return value + 10;
});
console.log(x);   // undefined
 
var y = array1.map((value,index) => {
    console.log(value);
    return value + 10;
});
console.log(y);   // [11, 12, 13, 14, 15]      
for in

经常用来迭代对象的属性或数组的每个元素,它包含当前属性的名称或当前数组元素的索引。

当遍历一个对象的时候,变量 i 是循环计数器 为 对象的属性名, 以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。

当遍历一个数组的时候,变量 i 是循环计数器 为 当前数组元素的索引

for..in循环会把某个类型的原型(prototype)中方法与属性给遍历出来.

const array = ["admin","manager","db"]; 
array.color = 'red';
array.prototype.name= "zhangshan"; 
for(var i in array){
    if(array.hasOwnProperty(i)){ 
        console.log(array[i]);  // admin,manager,db,color
    }
}
// hasOwnProperty(): 对象的属性或方法是非继承的,返回true      
for … of

迭代循环可迭代对象(包括Array,Map,Set,String,TypedArray,arguments 对象)等等。不能遍历对象。只循环集合本身的元素

var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
a.name = 'array';
for (var x of a) {
 console.log(x); //'A', 'B', 'C'
}
for (var x of s) {
 console.log(x);//'A', 'B', 'C'
}
for (var x of m) {
 console.log(x[0] + '=' + x[1]);//1='x',2='y',3='z'
}      

继承

// 定义一个动物类
function Animal(name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};      
原型链继承

核心: 将父类的实例作为子类的原型

function Dog(age) {
  this.age = age;
}
Dog.protoType = New Animal();
Dog.prototype.name = 'dog';

const dog = new Dog(12);
console.log(dog.name);
console.log(dog.eat('age'));
console.log(dog instanceof Animal); //true 
console.log(dog instanceof Dog); //true      

new 创建新实例对象经过了以下几步:

  1. 创建一个新对象
  2. 将新对象的_proto_指向构造函数的prototype对象
  3. 将构造函数的作用域赋值给新对象 (也就是this指向新对象)
  4. 执行构造函数中的代码(为这个新对象添加属性)
  5. 返回新的对象
// 1. 创建一个新对象
var Obj = {};
// 2. 将新对象的_proto_指向构造函数的prototype对象
Obj._proto_ =  Animal.prototype();
// 3. 执行构造函数中的代码(为这个新对象添加属性) 
Animal.call(Obj);
// 4. 返回新的对象
return Obj;      

重点:让新实例的原型等于父类的实例。

特点:

  1. 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性
  2. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  3. 父类新增原型方法/原型属性,子类都能访问到

    缺点:

  4. 新实例无法向父类构造函数传参。
  5. 继承单一。
  6. 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
  7. 要想为子类新增原型上的属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
构造函数继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Dog(name) {
  Animal.apply(this, 'dog');
  this.name = name;
}

const dog = new Dog();
console.log(dog.name);
console.log(dog.eat('age'));
console.log(dog instanceof Animal); //false 
console.log(dog instanceof Dog); //true      

重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

1、只继承了父类构造函数的属性,没有继承父类原型的属性。

2、解决了原型链继承缺点1、2、3。

3、可以实现多继承,继承多个构造函数属性(call多个)。

4、在子实例中可向父实例传参。

  1. 能继承父类构造函数的属性。
  2. 无法实现构造函数的复用。(每次用每次都要重新调用)
  3. 每个新实例都有父类构造函数的副本,臃肿。
  4. 实例并不是父类的实例,只是子类的实例
组合继承(原型链继承和构造函数继承)(常用)

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this, name);
  this.name = name;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true      

重点:结合了两种模式的优点,传参和复用

  1. 可以继承父类原型上的属性,可以传参,可复用。
  2. 每个新实例引入的构造函数属性是私有的。
  3. 既是子类的实例,也是父类的实例

调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

原型式继承

    

前端基础知识总结(一)

重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。

特点:类似于复制一个对象,用函数来包装。

1、所有实例都会继承原型上的属性。

2、无法实现复用。(新实例属性都是后面添加的)

寄生式继承

  

前端基础知识总结(一)

  

重点:就是给原型式继承外面套了个壳子。

优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。

寄生组合式继承(常用)
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

var cat = new Cat();
Cat.prototype.constructor = Cat; // 需要修复下构造函数
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true