js知识点

js知识点总结


一、js原型链

1、原型对象

为实例添加属性和方法,我们可以这样写:

	function Person () {
	    this.name = 'John';
	}
	var person = new Person();
	console.log(person.contructor);//f Person{xxx}
	console.log(Person.contructor);//f Function{xxx}
	console.log(Person.prototype);//{constructor:f}
	Person.prototype.say = function() {
	    console.log('Hello,' + this.name);
	};
	console.log(Person.prototype);//{constructor:f,say:f}
	person.say();//Hello,John
	person.say == new Person().say;//true

原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。其中,Person.prototype叫做原型对象

也可以这样写:

   function Person () {
        this.name = 'John';
    }
    var person = new Person();
    Person.prototype = {
        say: function() {
            console.log('Hello,' + this.name);
        }
    };
    person.say();//person.say is not a function
    //引用类型和值类型的区别

很不幸,person.say方法没有找到,所以报错了。其实这样写的初衷是好的:因为如果想在原型对象上添加更多的属性和方法,我们不得不每次都要写一行Person.prototype,还不如提炼成一个Object来的直接。但是此例子巧就巧在构造实例对象操作是在添加原型方法之前,这样就会造成一个问题:当var person = new Person()时,Person.prototype为:{f:constructor}。而对于实例person而言,其内部有一个原型链指针proto,该指针指向了Person.prototype指向的对象,即{f:constructor}。接下来重置了Person的原型对象,使其指向了另外一个对象,即Object {say: function},这时person.__proto__的指向还是没有变,它指向的{}对象里面是没有say方法的,因为报错。
从这个现象我们可以得出:在js中,对象在调用一个方法时会首先在自身里寻找是否有该方法,若没有,则去原型链上去寻找,依次层层递进,这里的原型链就是实例对象的__proto__属性。

原型对象的结构:

	Function.prototype = {
        constructor : Function,
        __proto__ : parent prototype,
        some prototype properties: ...
    };

总结:函数的原型对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针指向上一层的原型对象,而上一层的原型对象的结构依然类似,这样利用__proto__一直指向Object的原型对象上,而Object的原型对象用Object.prototype.proto = null表示原型链的最顶端,如此变形成了javascript的原型链继承,同时也解释了为什么所有的javascript对象都具有Object的基本方法。

下面这张图片是对原型链的说明
在这里插入图片描述


二、js继承方式

1、原型链继承
    // 父亲类
    function Parent() {
        this.value = 'value';
        //this.sayHi = function() {
        //	console.log('Hi');
		//}
    }
    Parent.prototype.sayHi = function() {
            console.log('Hi');
        }
    // 儿子类
    function Child() {

    }
    // 改变儿子的prototype属性为父亲的实例
    Child.prototype = new Parent();

    var child = new Child();
    // 首先现在child实例上进行查找,未找到,
    // 然后找到原型对象(Parent类的一个实例),在进行查找,未找到,
    // 在根据__proto__进行找到原型,发现sayHi方法。

    // 实现了Child继承
    child.sayHi();

但是这种方法存在一个问题:引用类型属性会被共享。

    // 父亲类
    function Parent() {
        this.color = ['pink', 'red'];
    }

    // 儿子类
    function Child() {

    }
    Child.prototype = new Parent();

    var child1 = new Child();
    var child2 = new Child();
    // 先输出child1和child2种color的值
    console.log(child1.color); // ["pink", "red"]
    console.log(child2.color); // ["pink", "red"]

    // 在child1的color数组添加white
    child1.color.push('white');
    console.log(child1.color); // ["pink", "red", "white"]
    // child1上的改动,child2也会受到影响
    console.log(child2.color); // ["pink", "red", "white"]
    console.log(Child.prototype.color);//["pink", "red", "white"]

从最后一句可看出可以看出,通过子类修改了父类的属性。

2、借用构造函数

在这里,我们借用call函数可以改变函数作用域的特性,在子类中调用父类构造函数,复制父类的属性。此时每调用一次子类,复制一次,每个实例都有自己的属性,不共享。同时我们可以通过call函数给父类传递参数。

解决引用类型共享问题:

    // 父亲类
    function Parent(name) {
        this.name = name;
        this.color = ['pink', 'red'];
    }

    // 儿子类
    function Child() {
        Parent.call(this);

        // 定义自己的属性
        this.value = 'test';
    }


    var child1 = new Child();
    var child2 = new Child();

    // 先输出child1和child2种color的值
    console.log(child1.color); // ["pink", "red"]
    console.log(child2.color); // ["pink", "red"]

    // 在child1的color数组添加white
    child1.color.push('white');
    console.log(child1.color); // ["pink", "red", "white"]
    // child1上的改动,child2并没有受到影响
    console.log(child2.color); // ["pink", "red"]

解决子类实例向超类构造函数传参问题:

    // 父亲类
    function Parent(name) {
        this.name = name;
        this.color = ['pink', 'red'];
    }

    // 儿子类
    function Child(name) {
        Parent.call(this, name);

        // 定义自己的属性
        this.value = 'test';
    }

    var child = new Child('qq');
    // 将qq传递给Parent,传递过去子类才获得了name属性
    console.log(child.name); // qq

当然,上述方法也存在一个问题,共享的方法都在构造函数中定义,无法达到函数复用的效果。

3、组合继承

根据上述两种方式,我们可以扬长避短,将需要共享的属性使用原型链继承的方法继承,将实例特有的属性,用借用构造函数的方式继承。

    // 父亲类
    function Parent() {
        this.color = ['pink', 'red'];
    }
    Parent.prototype.sayHi = function() {
        console.log('Hi');
    }

    // 儿子类
    function Child() {
        // 借用构造函数继承
        Parent.call(this);

        // 下面可以自己定义需要的属性
    }
    // 原型链继承
    Child.prototype = new Parent();

    var child1 = new Child();
    var child2 = new Child();

    // 每个实例特有的属性
    // 先输出child1和child2种color的值
    console.log(child1.color); // ["pink", "red"]
    console.log(child2.color); // ["pink", "red"]

    // 在child1的color数组添加white
    child1.color.push('white');
    console.log(child1.color); // ["pink", "red", "white"]
    // child1上的改动,child2并没有受到影响
    console.log(child2.color); // ["pink", "red"]

    // 每个实例共享的属性
    child1.sayHi(); // Hi
    child2.sayHi(); // Hi

上述方法,虽然综合了原型链和借用构造函数的优点,达到了我们想要的结果,但是它存在一个问题。就是创建一次实例时,两次调用了父类构造函数。

    // 父亲类
    function Parent() {
        this.color = ['pink', 'red'];
    }
    Parent.prototype.sayHi = function() {
        console.log('Hi');
    }

    // 儿子类
    function Child() {
        Parent.call(this); // 第二次调用构造函数:在新对象上创建一个color属性
    }
   
    Child.prototype = new Parent(); // 第一次调用构造函数Child.prototype将会得到一个color属性,屏蔽了原型中的color属性。

判断变量类型的方法

  • instanceof

instanceof 本意是用来判断 A 是否为 B 的实例对象,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。 在这里需要特别注意的是:instanceof检测的是原型,那它是怎么检测的呢,我们用一段伪代码来模拟其内部执行过程:

	instanceof (A,B) = {
	var L = A.__proto__;
	var R = B.prototype;
	if(L === R) {
	//A的内部属性__proto__指向B的原型对象
		return true;
	}
		return false;
	}

从上述过程可以看出,当A的__proto__ 指向B的prototype时,就认为A就是B的实例对象,我们再来看几个例子:

	[] instanceof Array; //true
	{} instanceof Object;//true
	new Date() instanceof Date;//true
	function Person(){};
	new Person() instanceof Person;
	[] instanceof Object; //true
	new Date() instanceof Object;//tru
	new Person instanceof Object;//true

从上面的例子中,我们发现虽然instanceof能够正确判断[] 是Array的实例对象,但不能辨别 [] 不是Object的实例对象,为什么呢,这还需要从javascript的原型链说起,我们首先来分析一下[]、Array、Object 三者之间的关系,从instanceof判断能够得出:[].proto ->Array.prototype, 而Array.prototype.__proto__指向了Object.prototype,Object.prototype.proto 指向了null,标志着原型链的结束。(ps:关于JS原型链请阅读:浅谈javascript原型和原型链) 因此,[]、Array、Object就形成了一条原型链:
在这里插入图片描述
从原型链可以看出,[]的__proto__最终指向了Object.prototype,类似的new Date()、new Person() 也会形成这样一条原型链,因此,我们用 instanceof 也不能完全精确的判断object类的具体数据类型。

  • Object.prototype.toString.call()
	Object.prototype.toString.call([])
	"[object Array]"
	Object.prototype.toString.call({})
	"[object Object]"
	Object.prototype.toString.call("")
	"[object String]"
	Object.prototype.toString.call(1)
	"[object Number]"
	Object.prototype.toString.call(function(){})
	"[object Function]"

该方法可以很好的判断变量的类型,唯一缺点就是过于复杂。


三、js闭包

1、闭包的三个特性

①函数嵌套函数
②函数内部可以引用函数外部的参数和变量
③参数和变量不会被垃圾回收机制回收

	function A(){
	    function B(){
	       console.log('Hello Closure!');
	    }
	    return B;
	}
	var C = A();
	C();// Hello Closure!

函数A的内部函数B被函数A外的一个变量 c 引用。把这句话再加工一下就变成了闭包的定义:当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。

2、闭包的用途

在了解闭包的作用之前,我们先了解一下 Javascript 中的 GC 机制:在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收,否则这个对象一直会保存在内存中。
在上述例子中,B 定义在 A 中,因此 B 依赖于 A ,而外部变量 C 又引用了 B , 所以A间接的被 C 引用。
也就是说,A 不会被 GC 回收,会一直保存在内存中。为了证明我们的推理,上面的例子稍作改进:

	function A() {
	    var count = 0;
	    function B() {
	       count ++;
	       console.log(count);
	    }
	    return B;
	}
	var C = A();
	C();// 1
	C();// 2
	C();// 3

count 是函数A 中的一个变量,它的值在函数B 中被改变,函数 B 每执行一次,count 的值就在原来的基础上累加 1 。因此,函数A中的 count 变量会一直保存在内存中。
当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。

	var aaa = (function(){
	    //变量作用域为函数内部,外部无法访问 
	    var a = 1;
	    function bbb(){
	        a++;
	        alert(a);
	    }
	    function ccc(){
	        a++;
	        alert(a);
	    }
	    return {
	        b:bbb,       //json结构
	        c:ccc
	    }
	})();
	aaa.b();   //2
	aaa.c()   //3

通过闭包访问私有属性。

3、闭包的高级写法
	(function (document) {
	    var viewport;
	    var obj = {
	        init: function(id) {
	           viewport = document.querySelector('#' + id);
	        },
	        addChild: function(child) {
	            viewport.appendChild(child);
	        },
	        removeChild: function(child) {
	            viewport.removeChild(child);
	        }
	    }
	    window.jView = obj;
	})(document);

这个组件的作用是:初始化一个容器,然后可以给这个容器添加子容器,也可以移除一个容器。功能很简单,但这里涉及到了另外一个概念:立即执行函数。 简单了解一下就行,需要重点理解的是这种写法是如何实现闭包功能的。
可以将上面的代码拆分成两部分:(function(){}) 和 () 。
第1个() 是一个表达式,而这个表达式本身是一个匿名函数,所以在这个表达式后面加 () 就表示执行这个匿名函数。
因此这段代码执行执行过程可以分解如下:

	var f = function(document) {
	    var viewport;
	    var obj = {
	        init: function(id) {
	            viewport = document.querySelector('#' + id);
	        },
	        addChild: function(child) {
	            viewport.appendChild(child);
	        },
	        removeChild: function(child) {
	            viewport.removeChild(child);
	        }
	    }
	    window.jView = obj;
	};
	f(document);

obj 是在函数 f 中定义的一个对象,这个对象中定义了一系列方法, 执行window.jView = obj 就是在 window 全局对象定义了一个变量 jView,并将这个变量指向 obj 对象,即全局变量 jView 引用了 obj . 而 obj 对象中的函数又引用了函数 f 中的变量 viewport ,因此函数 f 中的 viewport 不会被 GC 回收,viewport 会一直保存到内存中,所以这种写法满足了闭包的条件。

最后总结一下闭包的好处与坏处

好处:
①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)

坏处
①其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响


四、js事件循环

1、js是单线程并发语言

什么是单线程?
主程序只有一个线程,即同一时间片断内其只能执行单个任务。

为什么选择单线程?
JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

单线程意味着什么?
单线程就意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就需要一直等着。这就会导致IO操作(耗时但cpu闲置)时造成性能浪费的问题。

如何解决单线程带来的性能问题?

答案是异步!主线程完全可以不管IO操作,暂时挂起处于等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
注: 当主线程阻塞时,任务队列仍然是能够被推入任务的

2、js内存模型

在这里插入图片描述

  • 调用栈(Call Stack):用于主线程任务的执行
  • 堆(Heap): 堆表示一大块非结构化的内存区域,对象被存放在堆中
  • 任务队列(Queue): 用于存放异步任务与定时任务
	function boo (a) {
	    return a * 3
	}
	 function foo (b) {
	   return boo(4) * 2
	 }
	console.log(foo(3))

下面我们来分析一下上述代码的执行过程
(1)console.log(foo(3)) 执行,形成一个栈帧,调用foo函数,再形成另一个栈帧。
(2)新的栈帧压在上一个栈帧之上,继续执行代码,foo函数中又调用了boo函数,形成了另一个栈帧压在旧栈帧之上。然后执行boo。
(3)当执行完boo时候,返回值给foo函数之后,boo被推出调用栈,foo函数继续执行,然后foo函数执行完,返回值给console.log,foo函数被推出调用栈,console.log得到foo函数的返回值,运行,输出结果,最后console.log也被推出调用栈,该段程序执行完成。
在这里插入图片描述

3、代码运行机制
  • 所有同步任务都在主线程上的栈中执行。
  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"栈"中的所有同步任务执行完毕,系统就会读取"任务队列",选择出需要首先执行的任务(由浏览器决定,并不按序)。
4、事件循环

在这里插入图片描述
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的Event Loop(事件循环)。

在这里插入图片描述

但是js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的queue上拿宏任务的回掉函数。

	setTimeout(function(){
	    console.log('1')
	});

	new Promise(function(resolve){
	    console.log('2');
                   resolve();
	}).then(function(){
	    console.log('3')
	});

	console.log('4');

宏任务macrotask(事件队列中的每一个事件都是一个macrotask)
优先级:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval
比如:setImmediate指定的回调函数,总是排在setTimeout前面

微任务包括:
优先级:process.nextTick > Promise > MutationObserver

因为settimeout是宏任务,虽然先执行的他,但是他被放到了宏任务的eventqueue里面,然后代码继续往下检查看有没有微任务,检测到Promise的then函数把他放入了微任务序列。等到主线进程的所有代码执行结束后。先从微任务queue里拿回掉函数,然后微任务queue空了后再从宏任务的queue拿函数。

第一个宏任务(主程序)执行完,执行全部的微任务(一个promise),再执行下一个宏任务(settimeout)


五、js事件循环

1、Promise的立即执行性
	var p = new Promise(function(resolve, reject){
	  console.log("create a promise");
	  resolve("success");
	});
	
	console.log("after new Promise");
	
	p.then(function(value){
	  console.log(value);
	});

控制台输出:

	"create a promise"
	"after new Promise"
	"success"

Promise对象表示未来某个将要发生的事件,但在创建(new)Promise时,作为Promise参数传入的函数是会被立即执行的,只是其中执行的代码可以是异步代码。有些同学会认为,当Promise对象调用then方法时,Promise接收的函数才会执行,这是错误的。因此,代码中"create a promise"先于"after new Promise"输出。

2、Promise的三种状态
	var p1 = new Promise(function(resolve,reject){
	  resolve(1);
	});
	var p2 = new Promise(function(resolve,reject){
	  setTimeout(function(){
	    resolve(2);  
	  }, 500);      
	});
	var p3 = new Promise(function(resolve,reject){
	  setTimeout(function(){
	    reject(3);  
	  }, 500);      
	});
	
	console.log(p1);
	console.log(p2);
	console.log(p3);
	setTimeout(function(){
	  console.log(p2);
	}, 1000);
	setTimeout(function(){
	  console.log(p3);
	}, 1000);
	
	p1.then(function(value){
	  console.log(value);
	});
	p2.then(function(value){
	  console.log(value);
	});
	p3.catch(function(err){
	  console.log(err);
	});

控制台输出:

	Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 1}
	Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
	Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
	1
	2
	3
	Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 2}
	Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 3}

Promise的内部实现是一个状态机。Promise有三种状态:pending,resolved,rejected。当Promise刚创建完成时,处于pending状态;当Promise中的函数参数执行了resolve后,Promise由pending状态变成resolved状态;如果在Promise的函数参数中执行的不是resolve方法,而是reject方法,那么Promise会由pending状态变成rejected状态。
p2、p3刚创建完成时,控制台输出的这两台Promise都处于pending状态,但为什么p1是resolved状态呢? 这是因为p1 的函数参数中执行的是一段同步代码,Promise刚创建完成,resolve方法就已经被调用了,因而紧跟着的输出显示p1是resolved状态。我们通过两个setTimeout函数,延迟1s后再次输出p2、p3的状态,此时p2、p3已经执行完成,状态分别变成resolved和rejected。

3、Promise状态的不可逆性
	var p1 = new Promise(function(resolve, reject){
	  resolve("success1");
	  resolve("success2");
	});

	var p2 = new Promise(function(resolve, reject){
	  resolve("success");
	  reject("reject");
	});
	
	p1.then(function(value){
	  console.log(value);
	});
	
	p2.then(function(value){
	  console.log(value);
	});

控制台输出:

	"success1"
	"success"

Promise状态的一旦变成resolved或rejected时,Promise的状态和值就固定下来了,不论你后续再怎么调用resolve或reject方法,都不能改变它的状态和值。因此,p1中resolve(“success2”)并不能将p1的值更改为success2,p2中reject(“reject”)也不能将p2的状态由resolved改变为rejected.

4、链式调用
	var p = new Promise(function(resolve, reject){
	  resolve(1);
	});
	p.then(function(value){               //第一个then
	  console.log(value);
	  return value*2;
	}).then(function(value){              //第二个then
	  console.log(value);
	}).then(function(value){              //第三个then
	  console.log(value);
	  return Promise.resolve('resolve'); 
	}).then(function(value){              //第四个then
	  console.log(value);
	  return Promise.reject('reject');
	}).then(function(value){              //第五个then
	  console.log('resolve: '+ value);
	}, function(err){
	  console.log('reject: ' + err);
	})

控制台输出:

	1
	2
	undefined
	"resolve"
	"reject: reject"

Promise对象的then方法返回一个新的Promise对象,因此可以通过链式调用then方法。then方法接收两个函数作为参数,第一个参数是Promise执行成功时的回调,第二个参数是Promise执行失败时的回调。两个函数只会有一个被调用,函数的返回值将被用作创建then返回的Promise对象。这两个参数的返回值可以是以下三种情况中的一种:

  • return 一个同步的值 ,或者 undefined(当没有返回一个有效值时,默认返回undefined),then方法将返回一个resolved状态的Promise对象,Promise对象的值就是这个返回值。
  • return 另一个 Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回。
  • throw 一个同步异常,then方法将返回一个rejected状态的Promise, 值是该异常。

根据以上分析,代码中第一个then会返回一个值为2(1*2),状态为resolved的Promise对象,于是第二个then输出的值是2。第二个then中没有返回值,因此将返回默认的undefined,于是在第三个then中输出undefined。第三个then和第四个then中分别返回一个状态是resolved的Promise和一个状态是rejected的Promise,依次由第四个then中成功的回调函数和第五个then中失败的回调函数处理。

5.Promise then() 回调异步性
	var p = new Promise(function(resolve, reject){
	  resolve("success");
	});
	
	p.then(function(value){
	  console.log(value);
	});
	
	console.log("which one is called first ?");

控制台输出:

	"which one is called first ?"
	"success"

Promise接收的函数参数是同步执行的,但then方法中的回调函数执行则是异步的,因此,"success"会在后面输出。

6、Promise中的异常
	var p1 = new Promise( function(resolve,reject){
	  foo.bar();
	  resolve( 1 );	  //执行resolve()后不会立即跳到then
	});
	
	p1.then(
	  function(value){
	    console.log('p1 then value: ' + value);
	  },
	  function(err){
	    console.log('p1 then err: ' + err);
	  }
	).then(
	  function(value){
	    console.log('p1 then then value: '+value);
	  },
	  function(err){
	    console.log('p1 then then err: ' + err);
	  }
	);
	
	var p2 = new Promise(function(resolve,reject){
	  resolve( 2 );	
	});
	
	p2.then(
	  function(value){
	    console.log('p2 then value: ' + value);
	    foo.bar();
	  }, 
	  function(err){
	    console.log('p2 then err: ' + err);
	  }
	).then(
	  function(value){
	    console.log('p2 then then value: ' + value);
	  },
	  function(err){
	    console.log('p2 then then err: ' + err);
	    return 1;
	  }
	).then(
	  function(value){
	    console.log('p2 then then then value: ' + value);
	  },
	  function(err){
	    console.log('p2 then then then err: ' + err);
	  }
	);

控制台输出:

	p1 then err: ReferenceError: foo is not defined
	p2 then value: 2
	p1 then then value: undefined
	p2 then then err: ReferenceError: foo is not defined
	p2 then then then value: 1

Promise中的异常由then参数中第二个回调函数(Promise执行失败的回调)处理,异常信息将作为Promise的值。异常一旦得到处理,then返回的后续Promise对象将恢复正常,并会被Promise执行成功的回调函数处理。另外,需要注意p1、p2 多级then的回调函数是交替执行的 ,这正是由Promise then回调的异步性决定的。


六、js深拷贝和浅拷贝

1、浅拷贝

首先,浅拷贝和深拷贝都只针对于像Object, Array这样的复杂对象。
区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制。
面试官答案:浅拷贝是拷贝一层,深层次的对象级别的就拷贝引用;深拷贝是拷贝多层,每一级别的数据都会拷贝出来;
最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。

浅拷贝例子:

var obj = { a:1, arr: [2,3] };
var shallowObj = shallowCopy(obj);

function shallowCopy(src) {
  var newobj = {};
  for (var i in src) {
    if (src.hasOwnProperty(i)) {
      newobj[i] = src[i];
    }
  }
  return newobj;
}

因为浅复制只会将对象的各个属性进行依次复制,并不会进行递归复制,而 JavaScript 存储对象都是存地址的,所以浅复制会导致 obj.arr 和 shallowObj.arr 指向同一块内存地址。

2、深拷贝

深拷贝例子:

	var obj = { 
	    a:1, 
	    arr: [1,2],
	    nation : '中国',
	    birthplaces:['北京','上海','广州']
	};
	var obj2 = {name:'杨'};
	obj2 = deepCopy(obj,obj2);
	console.log(obj2);
	//深复制,要想达到深复制就需要用递归
	function deepCopy(original,result){
	   var result = result || {}for(var i in original){
	   if(typeof original[i] === 'object'){
	          //要考虑深复制问题了
	          if(original[i].constructor === Array){
	            //这是数组
	            result[i] =[]}else{
	            //这是对象
	            result[i] = {}}
	          deepCopy(original[i],result[i])}else{
	          result[i] = original[i]//一般数据直接拷贝
	        }
	     }
	     return result;
	 }

深复制则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。这就不会存在上面 obj 和 shallowObj 的 arr 属性指向同一个对象的问题。

3、实现深拷贝的方法
	//继承obj的属性,自身属性为空
	Object.create(obj)
	//只能实现单层拷贝{a:1,b:2},{a:{b:1}}无法实现
	Object.assign({},obj)   
	JSON.parse(JSON.stringify(obj));
	Array.concat() 
	Array.slice() 

七、js遍历数组与对象的方法

1、遍历数组

第一种:普通for循环

	for(j = 0; j < arr.length; j++) {
	   
	} 

简要说明: 最简单的一种,也是使用频率最高的一种,虽然性能不弱,但仍有优化空间。

第二种:优化版for循环

	for(j = 0,len=arr.length; j < len; j++) {
	   
	}

简要说明: 使用临时变量,将长度缓存起来,避免重复获取数组长度,当数组较大时优化效果才会比较明显。

第三种:弱化版for循环

	for(j = 0; arr[j]!=null; j++) {
	   
	}

简要说明: 这种方法其实严格上也属于for循环,只不过是没有使用length判断,而使用变量本身判断。

第四种:foreach循环

	arr.forEach(function(value,index,arr){  
	   //无返回值,可在循环中修改原数组
	});

简要说明: 数组自带的foreach循环,使用频率较高,实际上性能比普通for循环弱。

第五种:foreach变种

	Array.prototype.forEach.call(arr,function(el){  
	   
	});

简要说明: 由于foreach是Array型自带的,对于一些非这种类型的,无法直接使用(如NodeList),所以才有了这个变种,使用这个变种可以让类似的数组拥有foreach功能。实际性能要比普通foreach弱。

第六种:for in循环

	for(j in arr) {
	   //j代表下标,arr[j]代表数组元素
	}

简要说明: 这个循环很多人爱用,但实际上,经分析测试,在众多的循环遍历方式中它的效率是最低的。

第七种:map遍历

	arr.map(function(value,index,arr){  
	   //有返回值,可在循环中修改数组
	});

简要说明: 这种方式也是用的比较广泛的,虽然用起来比较优雅,但实际效率还比不上foreach。

第八种:for of遍历

	for(let value of arr) {  
	   
	});

简要说明: 这种方式是es6里面用到的,性能要好于forin,但仍然比不上普通for循环。

2、遍历对象

第一种:for in
其中i是属性名

	const obj = {
	            id:1,
	            name:'zhangsan',
	            age:18
	}
	
	 for(let key  in obj){
	        console.log(key + '---' + obj[key])  //id---1  name---'张三'  age:18
	  }

第二种:Object.key(obj) Object.value(obj)
参数:
obj:要返回其枚举自身属性的对象
返回值:
一个表示给定对象的所有可枚举属性的字符串数组。

	const obj = {
    id:1,
    name:'zhangsan',
    age:18
	}
	console.log(Object.keys(obj))     //['id','name','age']
	console.log(Object.values(obj))   //['1','zhangsan','18']

第三种:使用Object.getOwnPropertyNames(obj)
返回一个数组,包含对象自身的所有属性(包含不可枚举属性)
遍历可以获取key和value

	const obj = {
            id:1,
            name:'zhangsan',
            age:18
    }
    Object.getOwnPropertyNames(obj).forEach(function(key){
        console.log(key+ '---'+obj[key])
    })
    //id---1  name---'张三'  age:18

八、js字符串常用方法

1、concat
将两个或多个字符的文本组合起来,返回一个新的字符串。

	var a = "hello";
	var b = ",world";
	var c = a.concat(b);
	console.log(c); //"hello,world"

indexOf
返回字符串中一个子串第一处出现的索引(从左到右搜索)。如果没有匹配项,返回 -1 。

	var a = "hello";
	var b = ",world";
	var index1 = a.indexOf("l");
	//index1 = 2
	var index2 = a.indexOf("l",3);
	//index2 = 3

charAt
返回指定位置的字符。

	var a = "hello";
	var b = ",world";
	var get_char = a.charAt(0);
	//get_char = "h"

lastIndexOf
返回字符串中一个子串最后一处出现的索引(从右到左搜索),如果没有匹配项,返回 -1 。

	var a = "hello";
	var b = ",world";
	var index1 = lastIndexOf('l');
	//index1 = 3
	var index2 = lastIndexOf('l',2)
	//index2 = 2

match
检查一个字符串匹配一个正则表达式内容,如果么有匹配返回 null。

	var a = "hello";
	var b = ",world";
	var re = new RegExp(/^\w+$/);
	var is_alpha1 = a.match(re);
	//is_alpha1 = "hello"
	var is_alpha2 = b.match(re);
	//is_alpha2 = null

substring
返回字符串的一个子串,传入参数是起始位置和结束位置。

	var a = "hello";
	var b = ",world";
	var sub_string1 = a.substring(1);
	//sub_string1 = "ello"
	var sub_string2 = a.substring(1,4);
	//sub_string2 = "ell"

substr
返回字符串的一个子串,传入参数是起始位置和长度

	var a = "hello";
	var b = ",world";
	var sub_string1 = a.substr(1);
	//sub_string1 = "ello"
	var sub_string2 = a.substr(1,4);
	//sub_string2 = "ello"

replace
用来查找匹配一个正则表达式的字符串,然后使用新字符串代替匹配的字符串。

	var a = "hello";
	var b = ",world";
	var re = new RegExp(/^\w+$/);
	var result1 = a.replace(re,"Hello");
	//result1 = "Hello"
	var result2 = b.replace(re,"Hello");
	//result2 = ",world"

search
执行一个正则表达式匹配查找。如果查找成功,返回字符串中匹配的索引值。否则返回 -1 。

	var a = "hello";
	var b = ",world";
	var re = new RegExp(/^\w+$/);
	var index1 = a.search(re);
	//index1 = 0
	var index2 = b.search(re);
	//index2 = -1

slice
提取字符串的一部分,并返回一个新字符串(与 substring 相同)。

	var a = "hello";
	var b = ",world";
	var sub_string1 = a.slice(1);
	//sub_string1 = "ello"
	var sub_string2 = a.slice(1,4);
	//sub_string2 = "ell"

split
通过将字符串划分成子串,将一个字符串做成一个字符串数组。

	var a = "hello";
	var b = ",world";
	var arr1 = a.split("");
	//arr1 = [h,e,l,l,o]

length
返回字符串的长度,所谓字符串的长度是指其包含的字符的个数。

	var a = "hello";
	var b = ",world";
	var len = a.length();
	//len = 5

toLowerCase
将整个字符串转成小写字母。

	var a = "hello";
	var b = ",world";
	var lower_string = a.toLowerCase();
	//lower_string = "hello"

toUpperCase
将整个字符串转成大写字母。

	var a = "hello";
	var b = ",world";
	var upper_string = a.toUpperCase();
	//upper_string = "HELLO"

九、js数组常用方法

1.push(): 向数组尾部添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。

	var arr = [1,2,3];
	console.log(arr);        //  [1, 2, 3]
	var b = arr.push(4);  
	console.log(b);          //  4   //表示当前数组长度
	console.log(arr);        // [1, 2, 3, 4]   

pop(): 删除数组的最后一个元素,并返回该元素。注意,该方法会改变原数组。

	var arr = [1,2,3];
	console.log(arr);                 // [1,2,3]
	arr.pop();
	console.log( arr.pop() );       // [3]  //返回删除的元素
	console.log(arr);                // [1,2] 

2.unshift():在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。

	var arr = ['a', 'b', 'c'];
	arr.unshift('x');            // 4
	console.log(arr);        // ['x', 'a', 'b', 'c']

shift():删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。

	var arr = ['a', 'b', 'c'];
	arr.shift()         // 'a'
	console.log(arr)     // ['b', 'c']    

shift()方法还可以遍历并清空一个数组。

	var list = [1, 2, 3, 4, 5, 6];
	var item;
	while (item = list.shift()) {
	   console.log(item);
	}
	
	console.log(list);     // []

3.valueOf():返回数组的本身。

	var arr = [1, 2, 3];
	arr.valueOf()     // [1, 2, 3]   

indexOf():返回指定元素在数组中出现的位置,如果没有出现则返回-1。

	var arr = ['a', 'b', 'c'];
	arr.indexOf('b') // 1
	arr.indexOf('y') // -1

indexOf方法还可以接受第二个参数,表示搜索的开始位置。

	['a', 'b', 'c'].indexOf('a', 1)     // -1

上面代码从1号位置开始搜索字符a,结果为-1,表示没有搜索到。

toString():返回数组的字符串形式。

	var arr = [1, 2, 3];
	arr.toString()     // "1,2,3"
	var arr = [1, 2, 3, [4, 5, 6]];
	arr.toString()     // "1,2,3,4,5,6"

4.join():以参数作为分隔符,将所有数组成员组成一个字符串返回。如果不提供参数,默认用逗号分隔。

	var arr = [1, 2, 3, 4];
	arr.join(' ')      // '1 2 3 4'
	arr.join(' | ')     // "1 | 2 | 3 | 4"
	arr.join()          // "1,2,3,4"

5.concat():用于多个数组的合并。它将新数组的成员,添加到原数组的尾部,然后返回一个新数组,原数组不变。

	var arr = [1,2,3];
	var b = arr.concat([4,5,6]);
	console.log(b);        //[1,2,3,4,5,6]

6.reverse():用于颠倒数组中元素的顺序,返回改变后的数组。注意,该方法将改变原数组。

	var arr = ['a', 'b', 'c'];
	arr.reverse() // ["c", "b", "a"]
	console.log(arr) // ["c", "b", "a"]

7.slice():用于截取原数组的一部分,返回一个新数组,原数组不变。
slice(start,end)它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。

	var arr = ['a', 'b', 'c'];
	arr.slice(0)         // ["a", "b", "c"]
	arr.slice(1)         // ["b", "c"]
	arr.slice(1, 2)     // ["b"]
	arr.slice(2, 6)     // ["c"]
	arr.slice()           // ["a", "b", "c"]    无参数返回原数组
	arr.slice(-2)          // ["b", "c"]    参数是负数,则表示倒数计算的位置
	arr.slice(-2, -1)     // ["b"] 

8.splice():删除原数组的一部分成员,并可以在被删除的位置添加入新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
splice(start,delNum,addElement1,addElement2,…)第一个参数是删除的起始位置,第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。

	var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
	arr.splice(4, 2)     // ["e", "f"]  从原数组4号位置,删除了两个数组成员
	console.log(arr)     // ["a", "b", "c", "d"]
	var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
	arr.splice(4, 2, 1, 2)     // ["e", "f"]  原数组4号位置,删除了两个数组成员,又插入了两个新成员
	console.log(arr)         // ["a", "b", "c", "d", 1, 2]
	var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
	arr.splice(-4, 2)     // ["c", "d"]    起始位置如果是负数,就表示从倒数位置开始删除
	var arr = [1, 1, 1];
	arr.splice(1, 0, 2)     // []    如果只插入元素,第二个参数可以设为0
	conlose.log(arr)     // [1, 2, 1, 1]
	var arr = [1, 2, 3, 4];
	arr.splice(2)     // [3, 4] 如果只有第一个参数,等同于将原数组在指定位置拆分成两个数组
	console.log(arr)     // [1, 2]

9.sort():对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。

	['d', 'c', 'b', 'a'].sort()
	// ['a', 'b', 'c', 'd']
	[4, 3, 2, 1].sort()
	// [1, 2, 3, 4]
	[11, 101].sort()
	// [101, 11]
	[10111, 1101, 111].sort()
	// [10111, 1101, 111]

上面代码的最后两个例子,需要特殊注意。sort方法不是按照大小排序,而是按照对应字符串的字典顺序排序。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。
如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数,表示按照自定义方法进行排序。该函数本身又接受两个参数,表示进行比较的两个元素。如果返回值大于0,表示第一个元素排在第二个元素后面;其他情况下,都是第一个元素排在第二个元素前面。

	var arr = [10111, 1101, 111];
	arr.sort(function (a, b) {
	   return a - b;
	 })
	// [111, 1101, 10111]
	var arr1 = [
	   { name: "张三", age: 30 },
	   { name: "李四", age: 24 },
	   { name: "王五", age: 28 }
	]
	 
	arr1.sort(function (o1, o2) {
	   return o1.age - o2.age;
	 }) 
	 // [
	 //   { name: "李四", age: 24 },
	 //   { name: "王五", age: 28 },
	 //   { name: "张三", age: 30 }
	 // ]

10.map():对数组的所有成员依次调用一个函数,根据函数结果返回一个新数组。

	var numbers = [1, 2, 3];
	numbers.map(function (n) {
	   return n + 1;
	 });
	 // [2, 3, 4]
	numbers
	// [1, 2, 3]

上面代码中,numbers数组的所有成员都加上1,组成一个新数组返回,原数组没有变化。

11.filter():参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组。

	var arr = [1, 2, 3, 4, 5]
	arr.filter(function (elem) {
	   return (elem > 3);
	})
	// [4, 5]

十、iterator迭代器

下面是一段标准的for循环代码,通过变量i来跟踪colors数组的索引,循环每次执行时,如果i小于数组长度len则加1,并执行下一次循环

var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
    console.log(colors[i]);
}

虽然循环语句语法简单,但如果将多个循环嵌套则需要追踪多个变量,代码复杂度会大大增加,一不小心就错误使用了其他for循环的跟踪变量,从而导致程序出错。迭代器的出现旨在消除这种复杂性并减少循环中的错误。

迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value,表示下一个将要返回的值;另一个是done,它是一个布尔类型的值,当没有更多可返回数据时返回true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次next()方法,都会返回下一个可用的值。

如果在最后一个值返回后再调用next()方法,那么返回的对象中属性done的值为true,属性value则包含迭代器最终返回的值,这个返回值不是数据集的一部分,它与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回undefined。

下面用ES5的语法创建一个迭代器

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

在上面这段代码中,createIterator()方法返回的对象有一个next()方法,每次调用时,items数组的下一个值会作为value返回。当i为3时,done变为true;此时三元表达式会将value的值设置为undefined。最后两次调用的结果与ES6迭代器的最终返回机制类似,当数据集被用尽后会返回最终的内容

上面这个示例很复杂,而在ES6中,迭代器的编写规则也同样复杂,但ES6同时还引入了一个生成器对象,它可以让创建迭代器对象的过程变得更简单。


十一、generator生成器

generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次,生成器是一种返回迭代器的函数。

我们先复习函数的概念。一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果:

function foo(x) {
    return x + x;
}
var r = foo(1); // 调用foo函数

函数在执行过程中,如果没有遇到return语句(函数末尾如果没有return,就是隐含的return undefined;),控制权无法交回被调用的代码。

generator跟函数很像,定义如下:

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}

generator和函数不同的是,generator由function定义(注意多出的号),并且,除了return语句,还可以用yield返回多次。
大多数同学立刻就晕了,generator就是能够返回多次的“函数”?返回多次有啥用?

还是举个例子吧。
我们以一个著名的斐波那契数列为例,它由0,1开头:0 1 1 2 3 5 8 13 21 34 …
要编写一个产生斐波那契数列的函数,可以这么写:

function fib(max) {
    var
        t,
        a = 0,
        b = 1,
        arr = [0, 1];
    while (arr.length < max) {
        [a, b] = [b, a + b];
        arr.push(b);
    }
    return arr;
}
// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函数只能返回一次,所以必须返回一个Array。但是,如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它。

调用generator对象有两个方法,一是不断地调用generator对象的next()方法:

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}

next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。
当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。

第二个方法是直接用for … of循环迭代generator对象,这种方式不需要我们自己判断done:

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}
for (var x of fib(10)) {
    console.log(x); // 依次输出0, 1, 1, 2, 3, ...
}

generator和普通函数相比,有什么用?

因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。例如,用一个对象来保存状态,得这么写:

var fib = {
    a: 0,
    b: 1,
    n: 0,
    max: 5,
    next: function () {
        var
            r = this.a,
            t = this.a + this.b;
        this.a = this.b;
        this.b = t;
        if (this.n < this.max) {
            this.n ++;
            return r;
        } else {
            return undefined;
        }
    }
};

用对象的属性来保存状态,相当繁琐。

generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个好处要等到后面学了AJAX以后才能体会到。

没有generator之前的黑暗时代,用AJAX时需要这么写代码:

ajax('http://url-1', data1, function (err, result) {
    if (err) {
        return handle(err);
    }
    ajax('http://url-2', data2, function (err, result) {
        if (err) {
            return handle(err);
        }
        ajax('http://url-3', data3, function (err, result) {
            if (err) {
                return handle(err);
            }
            return success(result);
        });
    });
});

回调越多,代码越难看。

有了generator的美好时代,用AJAX时可以这么写:

try {
    r1 = yield ajax('http://url-1', data1);
    r2 = yield ajax('http://url-2', data2);
    r3 = yield ajax('http://url-3', data3);
    success(r3);
}
catch (err) {
    handle(err);
}

看上去是同步的代码,实际执行是异步的。


十二、for…in与for…of的区别

1.遍历数组通常用for循环

ES5的话也可以使用forEach,ES5具有遍历数组功能的还有map、filter、some、every、reduce、reduceRight等,只不过他们的返回结果不一样。但是使用foreach遍历数组的话,使用break不能中断循环,使用return也不能返回到外层函数。

Array.prototype.method=function(){
  console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组"
for (var index in myArray) {
  console.log(myArray[index]);
}

2.for in遍历数组的毛病

  • index索引为字符串型数字,不能直接进行几何运算
  • 遍历顺序有可能不是按照实际数组的内部顺序
  • 使用for in会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法method和name属性

所以for in更适合遍历对象,不要使用for in遍历数组。
那么除了使用for循环,如何更简单的正确的遍历数组达到我们的期望呢(即不遍历method和name),ES6中的for of更胜一筹.

Array.prototype.method=function(){
  console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组";
for (var value of myArray) {
  console.log(value);
}

记住,for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。
for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name

3.遍历对象
遍历对象 通常用for in来遍历对象的键名

Object.prototype.method=function(){
  console.log(this);
}
var myObject={
  a:1,
  b:2,
  c:3
}
for (var key in myObject) {
  console.log(key);
}

for in 可以遍历到myObject的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部判断一下,hasOwnPropery方法可以判断某属性是否是该对象的实例属性。

for (var key in myObject) {
  if(myObject.hasOwnProperty(key)){
    console.log(key);
  }
}

同样可以通过ES5的Object.keys(myObject)获取对象的实例属性组成的数组,不包括原型方法和属性

Object.prototype.method=function(){
  console.log(this);
}
var myObject={
  a:1,
  b:2,
  c:3
}
for (var key of Object.keys(myObject)) {
	console.log(key + ": " + someObject[key]);
}

总结

for…of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合.但是不能遍历对象,因为没有迭代器对象.与forEach()不同的是,它可以正确响应break、continue和return语句
for-of循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用for-in循环(这也是它的本职工作)或内建的Object.keys()方法:

for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

遍历map对象时适合用解构,例如;

for (var [key, value] of phoneBookMap) {
   console.log(key + "'s phone number is: " + value);
}

当你为对象添加myObject.toString()方法后,就可以将对象转化为字符串。
同样地,当你向任意对象添加myObjectSymbol.iterator方法,就可以遍历这个对象了。
举个例子,假设你正在使用jQuery,尽管你非常钟情于里面的.each()方法,但你还是想让jQuery对象也支持for-of循环,你可以这样做:

jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

所有拥有Symbol.iterator的对象被称为可迭代的。在接下来的文章中你会发现,可迭代对象的概念几乎贯穿于整门语言之中,不仅是for-of循环,还有Map和Set构造函数、解构赋值,以及新的展开操作符。

for…of的步骤
for-of循环首先调用集合的Symbol.iterator方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象;for-of循环将重复调用这个方法,每次循环调用一次。举个例子,这段代码是我能想出来的最简单的迭代器`:

var zeroesForeverIterator = {
 [Symbol.iterator]: function () {
   return this;
  },
  next: function () {
  return {done: false, value: 0};
 }
};

十三、ES6 Symbol

ES6中引入了一种新的基础数据类型:Symbol,不过很多开发者可能都不怎么了解它,或者觉得在实际的开发工作中并没有什么场景应用到它,那么今天我们来讲讲这个数据类型,并看看我们怎么来利用它来改进一下我们的代码。
这是一种新的基础数据类型(primitive type)
**Symbol是由ES6规范引入的一项新特性,它的功能类似于一种标识唯一性的ID。**通常情况下,我们可以通过调用Symbol()函数来创建一个Symbol实例:

let s1 = Symbol()

或者,你也可以在调用Symbol()函数时传入一个可选的字符串参数,相当于给你创建的Symbol实例一个描述信息:

let s2 = Symbol('another symbol')

由于Symbol是一种基础数据类型,所以当我们使用typeof去检查它的类型的时候,它会返回一个属于自己的类型symbol,而不是什么string、object之类的:

typeof s1  // 'symbol'

另外,我们需要重点记住的一点是:每个Symbol实例都是唯一的。因此,当你比较两个Symbol实例的时候,将总会返回false:

let s1 = Symbol()
let s2 = Symbol('another symbol')
let s3 = Symbol('another symbol')

s1 === s2 // false
s2 === s3 // false

应用场景1:使用Symbol来作为对象属性名(key)

在这之前,我们通常定义或访问对象的属性时都是使用字符串,比如下面的代码:

let obj = {
  abc: 123,
  "hello": "world"
}

obj["abc"] // 123
obj["hello"] // 'world'

而现在,Symbol可同样用于对象属性的定义和访问:

const PROP_NAME = Symbol()
const PROP_AGE = Symbol()

let obj = {
  [PROP_NAME]: "一斤代码"
}
obj[PROP_AGE] = 18

obj[PROP_NAME] // '一斤代码'
obj[PROP_AGE] // 18

随之而来的是另一个非常值得注意的问题:就是当使用了Symbol作为对象的属性key后,在对该对象进行key的枚举时,会有什么不同?在实际应用中,我们经常会需要使用Object.keys()或者for…in来枚举对象的属性名,那在这方面,Symbol类型的key表现的会有什么不同之处呢?来看以下示例代码:

let obj = {
   [Symbol('name')]: '一斤代码',
   age: 18,
   title: 'Engineer'
}

Object.keys(obj)   // ['age', 'title']

for (let p in obj) {
   console.log(p)   // 分别会输出:'age' 和 'title'
}

Object.getOwnPropertyNames(obj)   // ['age', 'title']

由上代码可知,Symbol类型的key是不能通过Object.keys()或者for…in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。
也正因为这样一个特性,当使用JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外:

JSON.stringify(obj)  // {"age":18,"title":"Engineer"}

我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。
然而,这样的话,我们就没办法获取以Symbol方式定义的对象属性了么?非也。还是会有一些专门针对Symbol的API,比如:

// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol(name)]

// 使用新增的反射API
Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']

应用场景2:使用Symbol来替代常量
先来看一下下面的代码,是不是在你的代码里经常会出现?

const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'
const TYPE_IMAGE = 'IMAGE'

function handleFileResource(resource) {
  switch(resource.type) {
    case TYPE_AUDIO:
      playAudio(resource)
      break
    case TYPE_VIDEO:
      playVideo(resource)
      break
    case TYPE_IMAGE:
      previewImage(resource)
      break
    default:
      throw new Error('Unknown type of resource')
  }
}

如上面的代码中那样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋一个唯一的值(比如这里的’AUDIO’、‘VIDEO’、 ‘IMAGE’),常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。
现在有了Symbol,我们大可不必这么麻烦了:

const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()

这样定义,直接就保证了三个常量的值是唯一的了!是不是挺方便的呢。

应用场景3:使用Symbol定义类的私有属性/方法
我们知道在JavaScript中,是没有如Java等面向对象语言的访问控制关键字private的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行API的设计时造成了一些困扰。
而有了Symbol以及模块化机制,类的私有属性和方法才变成可能。例如:

在文件 a.js中

const PASSWORD = Symbol()

class Login {
  constructor(username, password) {
    this.username = username
    this[PASSWORD] = password
  }

  checkPassword(pwd) {
      return this[PASSWORD] === pwd
  }
}

export default Login

在文件 b.js 中

import Login from './a'

const login = new Login('admin', '123456')

login.checkPassword('123456')  // true

login.PASSWORD  // oh!no!
login[PASSWORD] // oh!no!
login["PASSWORD"] // oh!no!

由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的),因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。


十四、Web Worker

大家都知道js是单线程的,在上一段js执行结束之前,后面的js绝对不会执行,那么为什么标题说js实现"多线程",虽然说加了引号,可是标题也不能乱写不是,可恶的标题党?

姑且抛开标题不说,先说我们经常会遇到的一个问题,假如我们页面中有很多js要执行,比如页面加载或点击某个按钮就会触发js,最坏的结果就是在很长的一段时间内用户都不能进行任何操作,所以,退出,关闭。。

当然上面说的有一些夸张,但是比如在移动端,我们都会想办法不停的提高页面性能,在某些情况下如果能有类似多线程的解决办法就更好了

html5 提出了一个名词:web Worker,按照官方的解释:web worker 是运行在后台的 JavaScript,不会影响页面的性能。也就可以理解为两段js同时执行,是不是也可以称呼为‘多线程’了呢。

今天主要来看一下基本用法:
首先是我们的前台页面,假设我们有一段计算的代码比较耗时,这时候我们需要后台的一个cal.js来计算。

在我们的前台页面,我们这样写:

   var worker=new Worker(cal.js'),
        i=100000000,
        ele=document.getElementById(‘btn’);
        //当点击某个按钮时,执行某个计算
        ele.addEventListener(‘click’,function(){
               worker.postMessage(i);
               worker.onmessage=function(e){
                        alert(e.data)
               }
        },false)

在cal.js中我们就进行计算

   onmessage=function(e){
           var data=e.data;
           for(i=0;i<data;i++){
                 //计算的代码
           }
         postMessage(data)
  }

这样一个简单的worker就实现了,通过点击按钮,后台的cal.js执行计算,而不会影响前台的操作;
简单总结一下,worker方法主要包括:

前台页面:

通过 new Worker( js) 加载一个JS文件来创建一个worker并返回一个worker实例。
通过worker.postMessage( data ) 方法来向worker发送数据。
通过worker.onmessage方法来接收worker发送过来的数据。
worker.terminate() 可以终止worker

后台js:

通过postMessage( data ) 方法来向主线程发送数据。
绑定onmessage方法来接收主线程发送过来的数据。


十六、new操作符和Object.creat()

给个例子看看

   function Person1(name){
        this.name = name;
    }
    function Person2(name){
        this.name = name;
        return this.name;
    }
    function Person3(name){
        this.name = name;
        return new String(name);
    }
    function Person4 (name){
        this.name = name;
        return function () {
        }
    }
    function Person5(name){
        this.name = name;
        return new Array();
    }
    const person1 = new Person1("yuer");//Person1 {name: "yuer"}

    const person2 = new Person2("yuer");//Person2 {name: "yuer"}

    const person3 = new Person3("yuer");//String {0: "y", 1: "u", 2: "e", 3: "r", length: 4, [[PrimitiveValue]]: "yuer"}

    const person4 = new Person4("yuer");//function() {}

    const person5 = new Person5("yuer");//[]

这里给出了5个例子,其实new操作符干了以下三步:
1.先创建了一个新的空对象
2.然后让这个空对象的__proto__指向函数的原型prototype
3.将对象作为函数的this传进去,如果return 出来东西是对象的话就直接返回 return 的内容,没有的话就返回创建的这个对象

var Func=function(){
};
var func=new Func ();

new共经过了4几个阶段

1、创建一个空对象

var obj= {};

2、设置原型链

obj.__proto__= Func.prototype;
//obj变成Func{}

3、让Func中的this指向obj,并执行Func的函数体。

var result =Func.call(obj); 
//Obj变成Func{xxx:undefined},有参数没有填写
//Obj变成Func{},无参数
//new的时候无参数  将构造函数Func的this指向obj对象,这样obj就可以访问到Func中的属性或方法
//Obj变成Func{xxx:xxx}
var result =Func.call(obj,arg); //new的时候有参数
//new的时候有参数  将构造函数Func的this指向obj对象,这样obj就可以访问到Func中的属性或方法
//call改变对象this指向的内容
function Obj(){this.value="对象!";}
var value="global 变量";
function Fun1(){alert(this.value);}

window.Fun1();   //global 变量
Fun1.call(window);  //global 变量
Fun1.call(document.getElementById('myText'));  //input text
Fun1.call(new Obj());   //对象!

4、判断Func的返回值类型:
如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

if (typeof(result) == "object"){
  func=result;
}
else{
    func=obj;;
}

Object.create时发生了什么?
Object.create()方法创建一个新对象,并使用现有的对象来提供新创建的对象的__proto__,关键代码如下

关键代码如下:

Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

可以看到Object.create内部创建了一个新对象,假设叫newObj,默认情况下newObj.proto== F.prototype,在本例中则重写了构造函数F的原型属性,最终的原型关系链为:

newObj.__proto__== F.prototype == o

在这里插入图片描述


十七、js垃圾回收机制

1、垃圾回收的必要性

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

这段话解释了为什么需要系统需要垃圾回收,JS不像C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:

var a = "before";
var b = "override a";
var a = b; //重写a

这段代码运行之后,“before”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。

2、垃圾回收原理浅析
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。
不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

1、标记清除

这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

function test(){ 
 var a = 10 ; //被标记 ,进入环境 
 var b = 20 ; //被标记 ,进入环境 
} 
test(); //执行完毕 之后 a、b又被标离开环境,被回收。

2、引用计数

另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

function test(){ 
   var a = {} ; //a的引用次数为0 
   var b = a ; //a的引用次数加1,为1 
   var c =a; //a的引用次数再加1,为2 
   var b ={}; //a的引用次数减1,为1 
}

但是用这种方法存在着一个问题,下面来看看代码:

function problem() {
    var objA = new Object();
    var objB = new Object();

    objA.someOtherObject = objB;
    objB.anotherObject = objA;
}

在这个例子中,objA和objB通过各自的属性相互引用;也就是说这两个对象的引用次数都是2。在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,objA和objB还将会继续存在,因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露。

我们知道,IE中有一部分对象并不是原生JavaScript对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object Model,组件对象)对象的形式实现的,而COM对象的垃圾回收器就是采用的引用计数的策略。因此,即使IE的Javascript引擎使用标记清除的策略来实现的,但JavaScript访问的COM对象依然是基于引用计数的策略的。说白了,只要IE中涉及COM对象,就会存在循环引用的问题。看看下面的这个简单的例子:

var element = document.getElementById("some_element");
var myObj =new Object();
myObj.element = element;
element.someObject = myObj;

上面这个例子中,在一个DOM元素(element)与一个原生JavaScript对象(myObj)之间建立了循环引用。其中,变量myObj有一个名为element的属性指向element;而变量element有一个名为someObject的属性回指到myObj。由于循环引用,即使将例子中的DOM从页面中移除,内存也永远不会回收。

不过上面的问题也不是不能解决,我们可以手动切断他们的循环引用。

myObj.element = null;
element.someObject =null;

这样写代码的话就可以解决循环引用的问题了,也就防止了内存泄露的问题。


十八、var let const

1.let添加了块级作用域
我们知道,JavaScript是没有块级作用域的,如果在块内使用var声明一个变量,它在代码块外面仍旧是可见的:

if (true) {
     var foo = 3;
}
console.log(foo);    // 3

for (var i = 0; i < 9; i++) {
     var j = i;
}
console.log(i);      // 9
console.log(j);      // 8

可以看到,在上面代码中,我们虽然是在块内声明的变量,但代码块执行完毕后,依然可以访问到相应的变量,说明JavaScript中没有块级作用域的。

而ES6规范给开发者带来了块级作用域,如果把var换成let命令,我们就可以获得一个块级变量:

if (true) {
     let foo = 3;
}
console.log(foo);   // Uncaught ReferenceError

for (let i = 0; i < 9; i++) {
     let j = i;
}
console.log(i);     // Uncaught ReferenceError
console.log(j);     // Uncaught ReferenceError

从上面代码可以看出,块内声明的变量,块外是不可见的,如果试图引用一个块内用let声明的变量,就会引发一个异常。

2.let约束了变量提升
在JavaScript中,变量提升是很常见的,例如下面这段代码:

function hoistVariable() {
    console.log('foo:', foo); // foo: undefined
    var foo = 3;
}

hoistVariable();

在代码正式执行之前,编译器将会对代码进行预编译分析阶段,在这个阶段,当前作用域中的变量和函数,将被提升到作用域的顶部。(注:目前的JavaScript引擎大都对源代码进行了编译处理,并且预编译和提升是抽象出来的概念。)

经过预编译之后的代码逻辑如下所示:

function hoistVariable() {
    var foo;
    console.log('foo:', foo); // foo: undefined
    foo = 3;
}

hoistVariable();

ES6中的let命令规范了变量的声明,约束了变量提升,也就是说,我们必须先声明,然后才可以使用,下面者段代码将会报错:

function nonHoistingFunc() {
    console.log('foo:', foo); // Uncaught ReferenceError
    let foo = 3;
}

nonHoistingFunc();

正确的使用方式是,永远将变量声明置于当前作用域顶部:

function nonHoistingFunc() {
    let foo = 3;
    console.log('foo:', foo); // 3
}

nonHoistingFunc();

需要注意的是,不管是var还是let,预编译过程中,都发生了变量提升,但与var不同的是,ES6对let进行了约束,其规定,在真正的词法变量声明之前,以任何方式访问let变量都是不允许的,所以从开发人员角度来看,let禁止了变量提升这一行为。

3.let有暂时性死区
只要在块内存在let命令,那么这个变量就绑定到了当前块作用域,不再受外部变量的影响,下面代码将会引发一个错误:

var foo = 3;

if (true) {
    foo = 5; // Uncaught ReferenceError
    let foo;
}

ES6规定如果块内存在let命令,那么这个块就会成为一个封闭的作用域,并要求let变量先声明才能使用,如果在声明之前就开始使用,它并不会引用外部的变量。

如果把这里的let替换成var,由于不会形成块级作用域,变量的声明其实是与第一行重复了,相当于下面这段代码:

var foo;

foo= 3;

if (true) {
    foo = 5;
}

4.let禁止重复声明变量
如上面所述,使用var可以重复声明变量,但let不允许在相同作用域内重复声明同一个变量,下面的代码会引发错误:

// SyntaxError
function func() {
    let foo = 3;
    var foo = 5;
}

// SyntaxError
function func() {
    let foo = 3;
    let foo = 5;
}

// SyntaxError
function func(arg) {
    let arg;
}

5.let不会成为全局对象的属性
我们在全局范围内使用var声明一个变量时,这个变量会自动成为全局对象的属性(在浏览器和Node.js环境下,这个全局对象分别是window和global),但let是独立存在的变量,不会成为全局对象的属性:

var a = 3;
console.log(window.a); // 3

let b = 5;
console.log(window.b); // undefined

6.const命令
以上let所介绍的规则均适用于const命令,不同的是,const声明的变量不能重新赋值,也是由于这个规则,const变量声明时必须初始化,不能留到以后赋值,所以下面的代码是不合法的:

const a = 3;

a = 5;   // Uncaught TypeError: Assignment to constant variable

const b; // Uncaught SyntaxError: Missing initializer in const declaration

const obj = {a:1,b:2}
obj.a = 3                //obj指向地址,地址没变,改变的是地址对应的内容

7.总结

var 声明的变量属于函数作用域,let 和 const 声明的变量属于块级作用域;
var 存在变量提升现象,而 let 和 const 没有此类现象;
var 变量可以重复声明,而在同一个块级作用域,let 、const变量不能重新声明,const 变量不能修改。

十九、async/await

先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 写一个async 函数

async function timeout() {
  return 'hello world';
}

语法很简单,就是在函数前面加上async 关键字,来表示它是异步的,那怎么调用呢?async 函数也是函数,平时我们怎么使用函数就怎么使用它,直接加括号调用就可以了,为了表示它没有阻塞它后面代码的执行,我们在async 函数调用之后加一句console.log;

async function timeout() {
    return 'hello world'
}
timeout();
console.log('虽然在后面,但是我先执行');

打开浏览器控制台,我们看到了
在这里插入图片描述
async 函数 timeout 调用了,但是没有任何输出,它不是应该返回 ‘hello world’, 先不要着急, 看一看timeout()执行返回了什么? 把上面的 timeout() 语句改为console.log(timeout())

async function timeout() {
    return 'hello world'
}
console.log(timeout());
console.log('虽然在后面,但是我先执行');

继续看控制台
在这里插入图片描述

原来async 函数返回的是一个promise 对象,如果要获取到promise 返回值,我们应该用then 方法, 继续修改代码

async function timeout() {
    return 'hello world'
}
timeout().then(result => {
    console.log(result);
})
console.log('虽然在后面,但是我先执行');

看控制台
在这里插入图片描述

我们获取到了"hello world’, 同时timeout 的执行也没有阻塞后面代码的执行,和 我们刚才说的一致。

这时,你可能注意到控制台中的Promise 有一个resolved,这是async 函数内部的实现原理。如果async 函数中有返回一个值 ,当调用该函数时,内部会调用Promise.resolve() 方法把它转化成一个promise 对象作为返回,但如果timeout 函数内部抛出错误呢? 那么就会调用Promise.reject() 返回一个promise 对象, 这时修改一下timeout 函数

async function timeout(flag) {
    if (flag) {
        return 'hello world'
    } else {
        throw 'my god, failure'
    }
}
console.log(timeout(true))  // 调用Promise.resolve() 返回promise 对象。
console.log(timeout(false)); // 调用Promise.reject() 返回promise 对象。

控制台如下:
在这里插入图片描述

如果函数内部抛出错误, promise 对象有一个catch 方法进行捕获。

timeout(false).catch(err => {
    console.log(err)
})

async 关键字差不多了,我们再来考虑await 关键字,await是等待的意思,那么它等待什么呢,它后面跟着什么呢?其实它后面可以放任何表达式,不过我们更多的是放一个返回promise 对象的表达式。注意await 关键字只能放到async 函数里面

现在写一个函数,让它返回promise 对象,该函数的作用是2s 之后让数值乘以2

// 2s 之后返回双倍的值
function doubleAfter2seconds(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2 * num)
        }, 2000);
    } )
}

现在再写一个async 函数,从而可以使用await 关键字, await 后面放置的就是返回promise对象的一个表达式,所以它后面可以写上 doubleAfter2seconds 函数的调用

async function testResult() {
    let result = await doubleAfter2seconds(30);
    console.log(result);
}

现在调用testResult 函数

testResult();

打开控制台,2s 之后,输出了60.

现在我们看看代码的执行过程,调用testResult 函数,它里面遇到了await, await 表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢?等后面的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。具体到 我们的代码, 遇到await 之后,代码就暂停执行了, 等待doubleAfter2seconds(30) 执行完毕,doubleAfter2seconds(30) 返回的promise 开始执行,2秒 之后,promise resolve 了, 并返回了值为60, 这时await 才拿到返回值60, 然后赋值给result, 暂停结束,代码才开始继续执行,执行 console.log语句。

下面看一组对比:
Promise链式调用

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

async/await等待方式

async function showAvatar() {
    // read our JSON
    let response = await fetch('/article/promise-chaining/user.json')
    let user = await response.json()
    
    // read github user
    let githubResponse = await fetch(`https://api.github.com/users/${user.name}`)
    let githubUser = await githubResponse.json()
    
    // 展示头像
    let img = document.createElement('img')
    img.src = githubUser.avatar_url
    img.className = 'promise-avatar-example'
    documenmt.body.append(img)
    
    // 等待3s
    await new Promise((resolve, reject) => {
        setTimeout(resolve, 3000)
    })
    
    img.remove()
    
    return githubUser
}
showAvatar()

二十、http无状态指的什么

stateless protocal

In computing, a stateless protocol is a communications protocol that treats each request as an independent transaction that is unrelated to any previous request so that the communication consists of independent pairs of request and response. A stateless protocol does not require the server to retain session information or status about each communications partner for the duration of multiple requests.

An example of a stateless protocol is HTTP.[1] The protocol provides no means of storing a user's data between requests.

http协议是无状态的是说http协议不记录历史的请求,每个请求相互独立。


二十一、箭头函数

我们在日常开发中,可能会需要写类似下面的代码

  const Person = {
    'name': 'little bear',
    'age': 18,
    'sayHello': function () {
      setInterval(function () {
        console.log('我叫' + this.name + '我今年' + this.age + '岁!')
      }, 1000)
    }
  }
  Person.sayHello()

上例的输出结果是我叫undefined,今年我undefined岁。为什么会输出这种结果呢?
这是因为setInterval执行的时候,是在全局作用域下的,所有this指向的是全局window,而window上没有name和age,所以当然输出的是undefined。不明白的同学可以去看看this的工作原理this。
那么,我们怎么要解决这个问题呢?
通常的写法是缓存this,然后在setInterval中用缓存的this进行操作,如下

 const Person = {
    'name': 'little bear',
    'age': 18,
    'sayHello': function () {
     let self = this
      setInterval(function () {
        console.log('我叫' + self.name + '我今年' + self.age + '岁!')
      }, 1000)
    }
  }
  const sayHelloFun = Person.sayHello
  sayHelloFun()

使用上叙方法,输出的结果就是我叫little bear,我今年18岁了。
那么,大家可能会觉得这样不科学,明明是写在对象里面的方法,为什么还要使用缓存这个对象才能正确使用。ECMA组织觉得这确实是个问题,之后在es6的新特性里添加了箭头函数,它能很好的解决这个问题。另外箭头函数还用简化代码量的特点。

1.什么是箭头函数
箭头函数的语法非常简单,看一下最简单的箭头函数表示法

() => console.log('Hello')

之前没有接触过箭头函数的人可能会惊讶于其代码的简洁。对比之前如果要写一个这样的函数

function(){
	console.log('hello')
}

箭头函数的简洁性一目了然。

2.和普通函数的区别
从上面的例子中,我们已经可以看出箭头函数的优势。
和普通函数相比,箭头函数主要就是以下两个方面的特点

不绑定this,arguments
更简化的代码语法
第二个特点不需要过多赘述,下面我们来看看不绑定this和arguments这两个特点

3.1 不绑定this
什么叫不绑定this,我个人的理解为箭头函数的this其实就是在定义的时候就确定好的,以后不管怎么调用这个箭头函数,箭头函数的this始终为定义时的this
我们还是以前面的那个setInterval代码为例

const Person = {
    'name': 'little bear',
    'age': 18,
    'sayHello': function () {
      setInterval(function () {
        console.log('我叫' + this.name + '我今年' + this.age + '岁!')
      }, 1000)
    }
Person.sayHello()

当Person.sayHello()去执行setInterval的时候,是在全局作用下执行的所有setInterval回调函数的this就为全局对象。

我们用箭头函数重写上诉函数

const Person = {
    'name': 'little bear',
    'age': 18,
    'sayHello': () => {
      setInterval(() => {
        console.log('我叫' + this.name + '我今年' + this.age + '岁!')
      }, 1000)
    }
Person.sayHello()

输出的是我叫’little bear’,今年18岁嘛?输出的还是undefined。为什么呢??
因为我把方法写在了对象里,而对象的括号是不能封闭作用域的。所以此时的this还是指向全局对象。
所以,通过以上的错误可以提醒我们,最好不要用箭头函数作为对象的方法。
我们需要重新举一个例子,如下

function Person () {
  this.name = 'little bear',
  this.age = 18
  let self = this
  setInterval(function sayHello () {
    console.log('我叫' + self.name + '我今年' + self.age + '岁!')
  }, 1000)
}
let p = new Person()

缓存this,然后输出,能达到我们想要的结果。
把上述例子改为箭头函数的形式如下

function Person () {
  this.name = 'little bear',
  this.age = 18
  setInterval(() => {
    console.log('我叫' + this.name + '我今年' + this.age + '岁')
},1000)
}
let p = new Person()

我们可以看到,箭头函数使用了定义时上下文的this,且与在哪里调用没有关系。

3.2 不绑定arguments
箭头函数还有一个比较有特点的地方就是其不绑定arguments,即如果你在箭头函数中使用arguments参数不能得到想要的内容。

let arrowfunc = () => console.log(arguments.length)
arrowfunc()
//output 
arguments is not defined

所以在箭头函数中我们是不能直接使用arguments对象的,但是如果我们又想获得函数的参数怎么办呢?
我们可以使用剩余参数来取代arguments剩余参数详情

let arrowfunc = (...theArgs) => console.log(theArgs.length)
arrowfunc(1,2)
//output
2

4.什么时候不能用箭头函数
前面我们已经看到了很多关于es6箭头函数的好处,也看到了箭头函数的一些不足。那么我们应该在什么时候使用箭头函数,而什么时候最好不要使用呢?
1.作为对象的方法
最好不要在对象的方法中使用箭头函数,这样可能会导致一些问题的产生。除非你很熟悉箭头函数。
2.不能作为构造函数
由于箭头函数的this不绑定的特点,所以不能使用箭头函数作为构造函数,实际上如果这样做了,也会报错。
3.定义原型方法

function Person (name){
this.name = name
}
Person.prototype.sayHello = () => {
    console.log(this)
}
var p1 = new Person()
p1.sayHello()
//output 
window对象


function Person (name){
this.name = name
}
Person.prototype.sayHello = function(){
    console.log(this)
}
var p1 = new Person()
p1.sayHello()
//output 
window对象

二十二、js argument参数

argument函数运行时的实参列表,是一个类数组对象(伪数组),callee是正在运行中的函数。

二十三、浏览器存储技术及其区别

互联网早期浏览器是没有状态维护,这个就导致一个问题就是服务器不知道浏览器的状态,无法判断是否是同一个浏览器。这样用户登录、购物车功能都无法实现。

三者区别:

  • cookie在浏览器请求中每次都会附加请求头中发送给服务器,大小不能超过4k。
  • localStorage保存数据会一直保存没有过期时间,不会随浏览器发送给服务器。大小5M或更大
  • sessionStorage仅当前页面有效一旦关闭就会被释放。也不会随浏览器发送给服务器。大小5M或更大

cookie
浏览器在本地按照一定规则存储一些文本字符串,每当浏览器像服务器发送请求时带这些字符串。服务器根据字符串判定浏览器的状态比如:登录、订单、皮肤。服务器就可以根据不同的cookie识别出不同的用户信息。浏览器和服务器cookie交互图如下。

cookie如何产生
1、在浏览器访问服务器时由服务器返回一个Set-Cookie响应头,当浏览器解析这个响应头时设置cookie
在这里插入图片描述
2、通过浏览器js脚本设置 document.cookie = ‘name=monsterooo’;

浏览器访问服务器携带cookie过程
在这里插入图片描述
js设置cookie详解
服务器设置cookie这里不过多介绍了同客户端js设置类似,重点来看一下js如何设置cookie和一些细节。

在js中设置cookie完整格式是:

document.cookie="key=value[; expires=date][; domain=domain][; path=path][; secure]"
  • key=value
    key设置的是cookie的键,value设置的是cookie的值。示例如下:
document.cookie = "name=monsterooo";

在这里插入图片描述

  • expires
    设置cookie的生存时间,默认为当然浏览器会话(Session)。当设置一个时间时,每次访问浏览器会用当前时间和cookie的expries做比对,如果过期cookie则会被删除。设置格式为GMT时间格式。示例如下:
    var t = new Date( +(new Date()) + 1000 * 120 );
    document.cookie = `name=monsterooo;expires=${t.toGMTString()};`;

在这里插入图片描述

 var t = new Date( +(new Date()) + 1000 * 120 );
 document.cookie = `name=monsterooo;expires=${t.toLocaleTimeString()}; domain=.example.com; path=/`;
  • path
    path路径和domain功能类似,只是path的范围更小。path控制cookie在当前域名的路径,只有路径相匹配cookie才能被读取到。在www.example.com/order/index.html中cookie设置如下 document.cookie = order=10; expires=${t.toGMTString()}; path=/order;,那么只有在/order路径下的页面cookie中才会带有order值。

localStorage和sessionStorage
localStorage和sessionStorage都继承于Storage,提供了统一的api来访问和设置数据。api列表为:

  • clear 清空存储中的所有本地存储数据
  • getItem 接受一个参数key,获取对应key的本地存储
  • key 接受一个整数索引,返回对应本地存储中索引的键
  • removeItem 接受一个参数key,删除对应本地存储的key
  • setItem 接受两个参数,key和value,如果不存在则添加,存在则更新。
localStorage.setItem('order', 'a109');
console.log(localStorage.key(0)); // order
console.log(localStorage.getItem('order')) // a109
localStorage.removeItem('order');
localStorage.clear();
// 对象访问方式同样有效
localStorage.order = 'b110';
localStorage.order; // b110

在这里插入图片描述

问题:是否可以使用localstorage代替Cookie

浏览器使用Cookie进行身份验证已经好多年,那现在既然localStorage存储空间那么大,是否可以把身份验证的数据直接移植过来呢。以现在来看,把身份验证数据使用localStorage进行存储还不太成熟。我们知道,通常可以使用XSS漏洞来获取到Cookie,然后用这个Cookie进行身份验证登录。后来为了防止通过XSS获取Cookie数据,浏览器支持了使用HTTPONLY来保护Cookie不被XSS攻击获取到。而localStorage存储没有对XSS攻击有任何的抵御机制。一旦出现XSS漏洞,那么存储在localStorage里的数据就极易被获取到。 所以在localstorage里面不建议存敏感信息。


二十四、浏览器缓存技术

浏览器第一次向一个web服务器发起http请求后,服务器会返回请求的资源,并且在响应头中添加一些有关缓存的字段如:Cache-Control、Expires、Last-Modified、ETag、Date等等。之后浏览器再向该服务器请求该资源就可以视情况使用强缓存和协商缓存。

  • 强缓存:浏览器直接从本地缓存中获取数据,不与服务器进行交互。
  • 协商缓存:浏览器发送请求到服务器,服务器判定是否可使用本地缓存。
  • 联系与区别:两种缓存方式最终使用的都是本地缓存;前者无需与服务器交互,后者需要。

下面假定浏览器已经访问了服务器,服务器返回了缓存相关的头部字段且浏览器已对相关资源做好缓存。通过下图来分析强缓存和协商缓存:
在这里插入图片描述
强缓存
如图红线所示的过程代表强缓存。用户发起了一个http请求后,浏览器发现先本地已有所请求资源的缓存,便开始检查缓存是否过期。有两个http头部字段控制缓存的有效期:Expires和Cache-Control,浏览器是根据以下两步来判定缓存是否过期的:

  • 查看缓存是否有Cache-Control的s-maxage或max-age指令,若有,则使用响应报文生成时间Date + s-maxage/max-age获得过期时间,再与当前时间进行对比(s-maxage适用于多用户使用的公共缓存服务器);
  • 如果没有Cache-Control的s-maxage或max-age指令,则比较Expires中的过期时间与当前时间。Expires是一个绝对时间。

注意,在HTTP/1.1中,当首部字段Cache-Control有指定s-maxage或max-age指令,比起首部字段Expires,会优先处理s-maxage或max-age。

另外下面列几个Cache-Control的常用指令:

  • no-cache:含义是不使用本地缓存,需要使用协商缓存,也就是先与服务器确认缓存是否可用。
  • no-store:禁用缓存。
  • public:表明其他用户也可使用缓存,适用于公共缓存服务器的情况。
  • private:表明只有特定用户才能使用缓存,适用于公共缓存服务器的情况。
    经过上述两步判断后,若缓存未过期,返回状态码为200,则直接从本地读取缓存,这就完成了整个强缓存过程;如果缓存过期,则进入协商缓存或服务器返回新资源过程。

协商缓存
当浏览器发现缓存过期后,缓存并不一定不能使用了,因为服务器端的资源可能仍然没有改变,所以需要与服务器协商,让服务器判断本地缓存是否还能使用。此时浏览器会判断缓存中是否有ETag或Last-Modified字段,如果没有,则发起一个http请求,服务器根据请求返回资源;如果有这两个字段,则在请求头中添加If-None-Match字段(有ETag字段的话添加)、If-Modified-Since字段(有Last-Modified字段的话添加)。注意:如果同时发送If-None-Match 、If-Modified-Since字段,服务器只要比较If-None-Match和ETag的内容是否一致即可;如果内容一致,服务器认为缓存仍然可用,则返回状态码304,浏览器直接读取本地缓存,这就完成了协商缓存的过程,也就是图中的蓝线;如果内容不一致,则视情况返回其他状态码,并返回所请求资源。下面详细解释下这个过程:

1.ETag和If-None-Match
二者的值都是服务器为每份资源分配的唯一标识字符串。

  • 浏览器请求资源,服务器会在响应报文头中加入ETag字段。资源更新时,服务器端的ETag值也随之更新;
  • 浏览器再次请求资源时,会在请求报文头中添加If-None-Match字段,它的值就是上次响应报文中的ETag的值;
  • 服务器会比对ETag与If-None-Match的值是否一致,如果不一致,服务器则接受请求,返回更新后的资源;如果一致,表明资源未更新,则返回状态码为304的响应,可继续使用本地缓存,要注意的是,此时响应头会加上ETag字段,即使它没有变化。

2.Last-Modified和If-Modified-Since
二者的值都是GMT格式的时间字符串。

  • 浏览器第一次向服务器请求资源后,服务器会在响应头中加上Last-Modified字段,表明该资源最后一次的修改时间;
  • 浏览器再次请求该资源时,会在请求报文头中添加If-Modified-Since字段,它的值就是上次服务器响应报文中的Last-Modified的值;
  • 服务器会比对Last-Modified与If-Modified-Since的值是否一致,如果不一致,服务器则接受请求,返回更新后的资源;如果一致,表明资源未更新,则返回状态码为304的响应,可继续使用本地缓存,与ETag不同的是:此时响应头中不会再添加Last-Modified字段。

3.ETag较之Last-Modified的优势

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要ETag呢?HTTP1.1中ETag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。

这时,利用ETag能够更加准确的控制缓存,因为ETag是服务器自动生成的资源在服务器端的唯一标识符,资源每次变动,都会生成新的ETag值。Last-Modified与ETag是可以一起使用的,但服务器会优先验证ETag。


二十五、浏览器的同源策略与跨域问题

什么是浏览器同源策略
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

它的核心就在于它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。

所谓同源是指:域名、协议、端口相同。

另外,同源策略又分为以下两种:

  • DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
  • XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。

为什么要有跨域限制
因为存在浏览器同源策略,所以才会有跨域问题。那么浏览器是出于何种原因会有跨域的限制呢。其实不难想到,跨域限制主要的目的就是为了用户的上网安全。如果浏览器没有同源策略,会存在什么样的安全问题呢。下面从 DOM 同源策略和 XMLHttpRequest 同源策略来举例说明:

如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:

做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank.com
把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
这时如果用户输入账号密码,我们的主网站可以跨域访问到 http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。

如果 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:

用户登录了自己的银行页面 http://mybank.comhttp://mybank.com 向用户的 cookie 中添加用户标识。
用户浏览了恶意页面 http://evil.com,执行了页面中的恶意 AJAX 请求代码。
http://evil.comhttp://mybank.com 发起 AJAX HTTP 请求,请求会默认把 http://mybank.com 对应 cookie 也同时发送过去。
银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据。此时数据就泄露了。
而且由于 Ajax 在后台执行,用户无法感知这一过程。
因此,有了浏览器同源策略,我们才能更安全的上网。

跨域的解决方法

从上面我们了解到了浏览器同源策略的作用,也正是有了跨域限制,才使我们能安全的上网。但是在实际中,有时候我们需要突破这样的限制,因此下面将介绍几种跨域的解决方法。

CORS(跨域资源共享)
CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。
1.请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

2.HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
    凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

简单请求

  • 在请求中需要附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。例如:Origin: http://www.laixiangran.cn
  • 如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发 )。例如:Access-Control-Allow-Origin:http://www.laixiangran.cn
  • 没有这个头部或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含 cookie 信息。
  • 如果需要包含 cookie 信息,ajax 请求需要设置 xhr 的属性 withCredentials 为 true,服务器需要设置响应头部 Access-Control-Allow-Credentials: true。

非简单请求
浏览器在发送真正的请求之前,会先发送一个 Preflight 请求给服务器,这种请求使用 OPTIONS 方法,发送下列头部:

  • Origin:与简单的请求相同。
  • Access-Control-Request-Method: 请求自身使用的方法。
  • Access-Control-Request-Headers: (可选)自定义的头部信息,多个头部以逗号分隔。

例如:

Origin: http://www.laixiangran.cn
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ

发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通:

Access-Control-Allow-Origin:与简单的请求相同。
Access-Control-Allow-Methods: 允许的方法,多个方法以逗号分隔。
Access-Control-Allow-Headers: 允许的头部,多个方法以逗号分隔。
Access-Control-Max-Age: 应该将这个 Preflight 请求缓存多长时间(以秒表示)。

例如:

Access-Control-Allow-Origin: http://www.laixiangran.cn
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000

一旦服务器通过 Preflight 请求允许该请求之后,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样了。

优点
CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护。
支持所有类型的 HTTP 请求。

缺点
存在兼容性问题,特别是 IE10 以下的浏览器。
第一次发送非简单请求时会多一次请求。

JSONP 跨域

由于 script 标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过动态创建 script 标签,然后利用 src 属性进行跨域,这也就是 JSONP 跨域的基本原理。

直接通过下面的例子来说明 JSONP 实现跨域的流程:

// 1. 定义一个 回调函数 handleResponse 用来接收返回的数据
function handleResponse(data) {
    console.log(data);
};
// 2. 动态创建一个 script 标签,并且告诉后端回调函数名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);
// 3. 
通过 script.src 请求 `http://www.laixiangran.cn/json?callback=handleResponse`// 4. 
后端能够识别这样的 URL 格式并处理该请求,然后返回 handleResponse({"name": "laixiangran"}) 给浏览器
// 5. 
浏览器在接收到 handleResponse({"name": "laixiangran"}) 之后立即执行 ,也就是执行 handleResponse 方法,获得后端返回的数据,这样就完成一次跨域请求了。

优点
使用简便,没有兼容性问题,目前最流行的一种跨域方法。

缺点
只支持 GET 请求。
由于是从其它域中加载代码执行,因此如果其他域不安全,很可能会在响应中夹带一些恶意代码。
要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 script 标签新增了一个 onerror 事件处理程序,但是存在兼容性问题。

图像 Ping 跨域

由于 img 标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过 img 标签的 src 属性进行跨域,这也就是图像 Ping 跨域的基本原理。

直接通过下面的例子来说明图像 Ping 实现跨域的流程:

var img = new Image();

// 通过 onload 及 onerror 事件可以知道响应是什么时候接收到的,但是不能获取响应文本
img.onload = img.onerror = function() {
    console.log("Done!");
}

// 请求数据通过查询字符串形式发送
img.src = 'http://www.laixiangran.cn/test?name=laixiangran';

优点
用于实现跟踪用户点击页面或动态广告曝光次数有较大的优势。

缺点
只支持 GET 请求。
只能浏览器与服务器的单向通信,因为浏览器不能访问服务器的响应文本。

服务器代理

浏览器有跨域限制,但是服务器不存在跨域问题,所以可以由服务器请求所要域的资源再返回给客户端。
服务器代理是万能的。

document.domain 跨域

对于主域名相同,而子域名不同的情况,可以使用 document.domain 来跨域。这种方式非常适用于 iframe 跨域的情况。

比如,有一个页面,它的地址是 http://www.laixiangran.cn/a.html,在这个页面里面有一个 iframe,它的 src 是 http://laixiangran.cn/b.html。很显然,这个页面与它里面的 iframe 框架是不同域的,所以我们是无法通过在页面中书写 js 代码来获取 iframe 中的东西的。

这个时候,document.domain 就可以派上用场了,我们只要把 http://www.laixiangran.cn/a.htmlhttp://laixiangran.cn/b.html 这两个页面的 document.domain 都设成相同的域名就可以了。但要注意的是,document.domain 的设置是有限制的,我们只能把 document.domain 设置成自身或更高一级的父域,且主域必须相同。例如:a.b.laixiangran.cn 中某个文档的 document.domain 可以设成 a.b.laixiangran.cnb.laixiangran.cnlaixiangran.cn 中的任意一个,但是不可以设成 c.a.b.laixiangran.cn ,因为这是当前域的子域,也不可以设成 baidu.com,因为主域已经不相同了。

例如,在页面 http://www.laixiangran.cn/a.html 中设置document.domain:

<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()">
<script>
    document.domain = 'laixiangran.cn'; // 设置成主域
    function test() {
        console.log(document.getElementById('myIframe').contentWindow);
    }
</script>

在页面 http://laixiangran.cn/b.html 中也设置 document.domain,而且这也是必须的,虽然这个文档的 domain 就是 laixiangran.cn,但是还是必须显式地设置 document.domain 的值:

<script>
    document.domain = 'laixiangran.cn'; // document.domain 设置成与主页面相同
</script>

这样,http://www.laixiangran.cn/a.html 就可以通过 js 访问到 http://laixiangran.cn/b.html 中的各种属性和对象了。

window.name 跨域

window 对象有个 name 属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面(不管是相同域的页面还是不同域的页面)都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

通过下面的例子介绍如何通过 window.name 来跨域获取数据的。

页面 http://www.laixiangran.cn/a.html 的代码:

<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
    // 2. iframe载入 "http://laixiangran.cn/b.html 页面后会执行该函数
    function test() {
        var iframe = document.getElementById('myIframe');
        
        // 重置 iframe 的 onload 事件程序,
        // 此时经过后面代码重置 src 之后,
        // http://www.laixiangran.cn/a.html 页面与该 iframe 在同一个源了,可以相互访问了
        iframe.onload = function() {
            var data = iframe.contentWindow.name; // 4. 获取 iframe 里的 window.name
            console.log(data); // hello world!
        };
        
        // 3. 重置一个与 http://www.laixiangran.cn/a.html 页面同源的页面
        iframe.src = 'http://www.laixiangran.cn/c.html';
    }
</script>

页面 http://laixiangran.cn/b.html 的代码:

<script type="text/javascript">
    // 1. 给当前的 window.name 设置一个 http://www.laixiangran.cn/a.html 页面想要得到的数据值 
    window.name = "hello world!";
</script>
location.hash 跨域

location.hash 方式跨域,是子框架具有修改父框架 src 的 hash 值,通过这个属性进行传递数据,且更改 hash 值,页面不会刷新。但是传递的数据的字节数是有限的。

页面 http://www.laixiangran.cn/a.html 的代码:

<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
    // 2. iframe载入 "http://laixiangran.cn/b.html 页面后会执行该函数
    function test() {
        // 3. 获取通过 http://laixiangran.cn/b.html 页面设置 hash 值
        var data = window.location.hash;
        console.log(data);
    }
</script>

页面 http://laixiangran.cn/b.html 的代码:

<script type="text/javascript">
    // 1. 设置父页面的 hash 值
    parent.location.hash = "world";
</script>
postMessage 跨域

window.postMessage(message,targetOrigin) 方法是 HTML5 新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源。这个应该就是以后解决 dom 跨域通用方法了。

调用 postMessage 方法的 window 对象是指要接收消息的那一个 window 对象,该方法的第一个参数 message 为要发送的消息,类型只能为字符串;第二个参数 targetOrigin 用来限定接收消息的那个 window 对象所在的域,如果不想限定域,可以使用通配符 *。

需要接收消息的 window 对象,可是通过监听自身的 message 事件来获取传过来的消息,消息内容储存在该事件对象的 data 属性中。

页面 http://www.laixiangran.cn/a.html 的代码:

<iframe src="http://laixiangran.cn/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
    // 1. iframe载入 "http://laixiangran.cn/b.html 页面后会执行该函数
    function test() {
        // 2. 获取 http://laixiangran.cn/b.html 页面的 window 对象,
        // 然后通过 postMessage 向 http://laixiangran.cn/b.html 页面发送消息
        var iframe = document.getElementById('myIframe');
        var win = iframe.contentWindow;
        win.postMessage('我是来自 http://www.laixiangran.cn/a.html 页面的消息', '*');
    }
</script>

页面 http://laixiangran.cn/b.html 的代码:

<script type="text/javascript">
    // 注册 message 事件用来接收消息
    window.onmessage = function(e) {
        e = e || event; // 获取事件对象
        console.log(e.data); // 通过 data 属性得到发送来的消息
    }
</script>

二十六、call、bind、apply的区别

call和apply可以改变this指向,可以在一个对象中调用另一个对象的方法。bind同apply,不是立即执行,可以先绑定到点击事件,用来触发事件。

call、apply、bind的作用是改变函数运行时this的指向,所以先说清楚this。
以下是函数的调用方法:

方法调用模式:
当一个函数被保存为对象的一个方法时,如果调用表达式包含一个提取属性的动作,那么它就是被当做一个方法来调用,此时的this被绑定到这个对象。

    var a = 1
    var obj1 = {
      a:2,
      fn:function(){
        console.log(this.a)
      }
    }
    obj1.fn()//2    

此时的this是指obj1这个对象,obj1.fn()实际上是obj1.fn.call(obj1),事实上谁调用这个函数,this就是谁。补充一下,DOM对象绑定事件也属于方法调用模式,因此它绑定的this就是事件源DOM对象。如:

document.addEventListener('click', function(e){
    console.log(this);
    setTimeout(function(){
        console.log(this);
    }, 200);
}, false);

点击页面,依次输出:document和window对象
解析:点击页面监听click事件属于方法调用,this指向事件源DOM对象,即obj.fn.apply(obj),setTimeout内的函数属于回调函数,可以这么理解,f1.call(null,f2),所以this指向window。

函数调用模式:
就是普通函数的调用,此时的this被绑定到window

function fn1(){
      console.log(this)//window
    }
fn1()

函数嵌套

function fn1(){
    function fn2(){
        console.log(this)//window
    }
    fn2()
}
fn1()

把函数赋值之后再调用

var a = 1
var obj1 = {
    a:2,
    fn:function(){
        console.log(this.a)
    }
}
var fn1 = obj1.fn
fn1()//1

obj1.fn是一个函数function(){console.log(this.a)},此时fn1就是不带任何修饰的函数调用,function(){console.log(this.a)}.call(undefined),按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则:

如果你传的 context 就 null 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)
因此上面的this绑定的就是window,它也被称为隐性绑定。
如果你希望打印出2,可以修改fn1()为fn1.call(obj1),显示地绑定this为obj1

回调函数

var a = 1
function f1(fn){
    fn()
    console.log(a)//1
}
f1(f2)

function f2(){
    var a = 2
}

改写代码如下:

var a = 1
function f1(){
    (function (){var a = 2})()
    console.log(a)//1
}
f1()

仍旧是最普通的函数调用,f1.call(undefined),this指向window,打印出的是全局的a。
借此,我们终于可以解释为什么setTimeout总是丢失this了,因为它也就是一个回调函数而已。

setTimeout(function() {
    console.log(this)//window
    function fn(){
        console.log(this)//window
    }
    fn()
}, 0);

构造器调用模式:
new一个函数时,背地里会将创建一个连接到prototype成员的新对象,同时this会被绑定到那个新对象上

function Person(name,age){
// 这里的this都指向实例
    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}

var dot = new Person('Dot',2)
dot.sayAge()//2

call
call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表。当第一个参数为null、undefined的时候,默认指向window。

var arr = [1, 2, 3, 89, 46]
var max = Math.max.call(null, arr[0], arr[1], arr[2], arr[3], arr[4])          //89
var obj = {
    message: 'My name is: '
}

function getName(firstName, lastName) {
    console.log(this.message + firstName + ' ' + lastName)
}

getName.call(obj, 'Dot', 'Dolby')

apply
apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。

var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89

是不是觉得和前面写的call用法很像,事实上apply 和 call 的用法几乎相同, 唯一的差别在于:当函数需要传递多个变量时, apply 可以接受一个数组作为参数输入, call 则是接受一系列的单独变量。
看一个例子:

var obj = {
    message: 'My name is: '
}

function getName(firstName, lastName) {
    console.log(this.message + firstName + ' ' + lastName)
}

getName.apply(obj, ['Dot', 'Dolby'])// My name is: Dot Dolby

可以看到,obj 是作为函数上下文的对象,函数 getName 中 this 指向了 obj 这个对象。参数 firstName 和 lastName 是放在数组中传入 getName 函数。
call和apply可用来借用别的对象的方法,这里以call()为例:

var Person1  = function () {
    this.name = 'Dot';
}
var Person2 = function () {
    this.getname = function () {
        console.log(this.name);
    }
    Person1.call(this);
}
var person = new Person2();
person.getname();       // Dot

从上面我们看到,Person2 实例化出来的对象 person 通过 getname 方法拿到了 Person1 中的 name。因为在 Person2 中,Person1.call(this) 的作用就是使用 Person1 对象代替 this 对象,那么 Person2 就有了 Person1 中的所有属性和方法了,相当于 Person2 继承了 Person1 的属性和方法。
对于什么时候该用什么方法,其实不用纠结。如果你的参数本来就存在一个数组中,那自然就用 apply,如果参数比较散乱相互之间没什么关联,就用 call。像上面的找一组数中最大值的例子,当然是用apply合理。

bind
和call很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用

bind返回值是函数

var obj = {
    name: 'Dot'
}

function printName() {
    console.log(this.name)
}

var dot = printName.bind(obj)
console.log(dot) // function () { … }
dot()  // Dot

bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 printName 中的 this 并没有被改变,依旧指向全局对象 window。

参数的使用

function fn(a, b, c) {
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');

fn('A', 'B', 'C');            // A B C
fn1('A', 'B', 'C');           // Dot A B
fn1('B', 'C');                // Dot B C
fn.call(null, 'Dot');      // Dot undefined undefined

call 是把第二个及以后的参数作为 fn 方法的实参传进去,而 fn1 方法的实参实则是在 bind 中参数的基础上再往后排。
有时候我们也用bind方法实现函数珂里化,以下是一个简单的示例:

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

27、事件委托、事件冒泡、事件捕获

事件的绑定

要想让 JavaScript 对用户的操作作出响应,首先要对 DOM 元素绑定事件处理函数。所谓事件处理函数,就是处理用户操作的函数,不同的操作对应不同的名称。

在JavaScript中,有三种常用的绑定事件的方法:

1.DOM元素中直接绑定(注册行内事件)2. 在JavaScript代码中绑定;
3. 绑定事件监听函数

1.1 在DOM中直接绑定事件

我们可以在DOM元素上绑定onclick、onmouseover、onmouseout、onmousedown、onmouseup、ondblclick、onkeydown、onkeypress、onkeyup等 DOM事件 。

//先阻止a标签的默认行为,再绑定点击事件
<a href="javascript:;"  onclick="hello()">

<script>
function hello(){
	 alert("hello world!");
}
</script>

1.2 在JavaScript代码中绑定事件

在JavaScript代码中(即 script 标签内)绑定事件可以使JavaScript代码与HTML标签分离,文档结构清晰,便于管理和开发。

//先阻止a标签的默认行为,再绑定点击事件
<a href="javascript:;"  id="a-btn">

<script>
 	document.querySelector('#a-btn').onclick = function(e){
 		//阻止默认行为
 		e = e || window.event;
 		e.preventDefault();
		
		//功能代码 doSomething......
		alert("hello world!");
 	};
</script>

1.3 使用事件监听绑定事件
可以给同名的事件,绑定多个事件处理程序

    语法:对象.addEventListener(参数1,参数2,参数3);
    参数1:事件名(字符串),不要加on  例如:click  、 mouseover 、mouseout
    参数2:事件处理程序(函数名),当事件触发后哪个函数来处理
    参数3:是一个bool类型,可以不传,默认为fasle(代表冒泡)跟冒泡和捕获有关
    如果是true则表示捕获阶段
    
    如果有同名事件不会覆盖,而是会依次执行
    IE8及以前的版本不支持

1.3.1 事件监听的好处

  1. 可以绑定多个事件,常规的事件绑定只执行最后绑定的事件。
  2. 可以解除相应的绑定
//绑定多个事件
 	btn.addEventListener('click',function (  ) {
         alert('11111');
     },false);
    
    btn.addEventListener('click',function (  ) {
        alert('2222');
    },false);
//解除事件的绑定
<input type="button" value="click me" id="btn5">
<script>
	var btn5 = document.getElementById("btn5");
	btn5.addEventListener("click",hello1);//执行了
	btn5.addEventListener("click",hello2);//不执行
	btn5.removeEventListener("click",hello2);
	
	function hello1(){
	 alert("hello 1");
	}
	function hello2(){
	 alert("hello 2");
	}
</script>

事件冒泡与事件捕获:

起初Netscape制定了JavaScript的一套事件驱动机制(即事件捕获)。随即IE也推出了自己的一套事件驱动机制(即事件冒泡)。最后W3C规范了两种事件机制,分为捕获阶段、目标阶段、冒泡阶段。IE8以前IE一直坚持自己的事件机制(前端人员一直头痛的兼容性问题),IE9以后IE也支持了W3C规范。

在这里插入图片描述

在这里插入图片描述
这就说明:
当一个DOM事件被触发时,它不仅仅只是单纯地在本身对象上触发一次,而是会经历三个不同的阶段:

1. 捕获阶段 先由文档的根节点document往事件触发对象,从外向内捕获事件对象;
2. 目标阶段 到达目标事件位置(事发地),触发事件;
3. 冒泡阶段 再从目标事件位置往文档的根节点方向回溯,从内向外冒泡事件对象。

2.1 事件冒泡:

如果一个元素的事件被触发,那么它所有的父级元素的同名事件都会被触发.

注意点:只有当父级拥有同名事件的时候才会被触发.

window.onclick = function () {

        alert("window被点击了");
    }

    document.onclick = function () {

        alert("文档被点击了");
    }

    document.documentElement.onclick = function () {

        alert("html被点击了");
    }

    document.body.onclick = function () {

        alert("body被点击了");
    }

    document.getElementById("box").onclick = function () {

        alert("我是骚粉的大盒子");
    };

上述代码的执行顺序是:
box元素–> body–>HTML–>document–>window

2.2 事件捕获:
从最顶级的父元素一级一级往下找子元素触发同名事件,直到触发事件的元素为止.

注意点1:是去寻找与与父元素注册的同名事件的子元素
注意点2:因为事件捕获,只能通过addEventListener并且参数写true才是事件捕获
注意点3:其他情况都是冒泡(不是通过addEventListener添加、addEventListener参数为false)

window.addEventListener("click", function () {
        alert("这是window");
    },true)


    document.addEventListener("click", function () {
        alert("这是document");
    },true)

    document.documentElement.addEventListener("click", function (e) {
        e = e || window.event;
        alert("这是html");
//        e.stopPropagation();//阻止事件冒泡和事件捕获

    },true)

    document.body.addEventListener("click", function () {

        alert("这是body");

    },true)

    //参数3:默认是false,代表是支持事件冒泡
    box.addEventListener("click", function () {

        alert("这是box");
    },true)

上述代码的执行顺序是:
window–>document–>HTML–>body–>box元素

我们知道了事件捕获以及事件的冒泡原理,但是有的时候,我们就只想让其子元素单独触发或者让其父元素单独触发,此时就需要用到阻止事件的捕获以及冒泡的方法.

1.阻止事件冒泡:让同名事件不要在父元素中冒泡(触发)
说人话:点击一个元素只会触发当前元素的事件,不会触发父元素的同名事件
语法:  事件对象.stopPropagation()       IE8及之前不支持
       事件对象.cancelBubble = true      IE8之前支持
     
注意:如果想要阻止事件冒泡,一定要在触发事件的函数中接收事件对象


2.阻止事件捕获:
事件对象.stopPropagation() 除了可以阻止冒泡还可以阻止捕获

IE8及以前没有捕获!

2.2.1 利用事件的冒泡写一个案例:

需求:移入li标签颜色改变.
思路:如果想给父元素的多个子元素添加事件,我们可以只需要给父元素添加事件即可,然后通过获取事件源(e.target)就可以得知是哪一个子元素触发了这个事件

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        ul {
            border: 1px solid red;
            width: 500px;
            text-align: center;
            margin: 100px auto;
        }
        #ul1 li {
            list-style: none;
            line-height: 30px;
            font-size: 20px;
            border: 1px solid #ccc;

        }
    </style>
</head>

<body>

<ul id="ul1">
    <li>我是第1个li标签</li>
    <li>我是第2个li标签</li>
    <li>我是第3个li标签</li>
    <li>我是第4个li标签</li>
    <li>我是第5个li标签</li>
    <li>我是第6个li标签</li>
    <li>我是第7个li标签</li>
    <li>我是第8个li标签</li>
    <li>我是第9个li标签</li>
    <li>我是第10个li标签</li>
</ul>

<script>
    //需求: 1.给10个li标签移入添加背景高亮
    //      2.移出背景颜色恢复

    //思路:
    // 给ul注册鼠标移入事件,通过获取事件源(e.target)就可以得知是哪一个子元素触发了这个事件

    var ul1 = document.getElementById("ul1");

    var DefalutColor = null;  //全局变量存储默认的背景色
    ul1.onmouseover = function(e){
        //获取到的是对应的li标签对象
        // console.log(e.target);
        DefalutColor = e.target.style.backgroundColor; //将li标签默认的背景色赋给defalutColor
        e.target.style.backgroundColor = 'yellow';
    }

    ul1.onmouseout = function(e){
        e.target.style.backgroundColor = DefalutColor; //li标签默认的背景色
    }

    
</script>
</body>
</html>

3.3 事件委托原理

利用的就是前面讲的冒泡原理:比如给li点击事件,事件先开始捕获阶段,从body->ul->li,而li是目标元素,此时处于目标阶段,浏览器就会查看是否有点击事件,发现没有,那么进入第三个阶段冒泡,又从li->ul,发现ul身上有点击事件,那么便触发ul的点击事件.

3.4 实例实现事件委托

每一个按钮拥有点击事件,但是注册的时候却只给父元素div注册点击事件,同时实现点击不同按钮操作功能不同.

<div id="box">
  <input type="button" id="add" value="添加" />
    <input type="button" id="remove" value="删除" />
    <input type="button" id="move" value="移动" />
    <input type="button" id="select" value="选择" />
</div>
//js代码
window.onload = function(){
   		//父元素
           var Box = document.getElementById("box");
           Box.onclick = function (e) {
               var e = e || window.event;
               var target = e.target;
               if(target.nodeName.toLocaleLowerCase() == 'input'){
                   switch(target.id){
                       case 'add' :
                           alert('添加');
                           break;
                       case 'remove' :
                           alert('删除');
                           break;
                       case 'move' :
                           alert('移动');
                           break;
                       case 'select' :
                           alert('选择');
                           break;
                   }
               }
           }
           
       }

28、Ajax的五个步骤

1.建立xmlHttpRequest对象
2.设置回调函数
3.设置请求头
4.使用open方法与服务器建立连接
5.使用send方法发送请求
6.在回调函数中根据不同响应处理数据


29、get和post的区别

最直接的区别,get请求的参数是放在url里的,post请求参数是放在body里的;
Get请求的参数只能是ascii码,所以中文需要url编码;post没有这个限制
Get请求的url传参有长度限制,而post请求没有长度限制。

其实,GET和POST本质上两者没有任何区别。他们都是HTTP协议中的请求方法。底层实现都是基于TCP/IP协议。上述的所谓区别,只是浏览器厂家根据约定,做得限制而已。


30、输入一个url按下回车发生了什么

大致可分为两个过程:网络通信和页面渲染。

一、网络通信
互联网整个网络设备间的通信都必须遵守TCP/IP协议标准。利用TCP/IP进行通信时,会通过分层顺序与对端进行通信,发送数据会依次经过应用层,传输层,网络层,数据链路层,物理层。而接受数据的顺序刚好相反。
在这里插入图片描述

各层数据包封装报头信息如下

步骤:
1.在应用层输入url

用户输入https://mp.csdn.net时,其中http表示采用http协议进行传输,mp.csdn.net为网络地址,表示请求的资源在那个位置(主机)。一般网络地址为IP地址,此处为域名,是IP地址的包装。为了让用户方便使用。

2.应用层DNS解析域名,获得对端IP地址

计算机在通信时是通过IP地址辨识,而不是域名。
域名查找顺序:本地缓存->系统缓存->浏览器缓存->ISP缓存->根域名服务器->主域名服务器

如果本地缓存有就直接使用,并不是每一个过程都需要走。没有才继续往下走。直到获得IP地址。

3.应用层客户端发送HTTP请求

HTTP请求包括请求报头和请求主体。其中请求报头中包含了请求方法,请求资源,请求所使用放入协议(http,smtp等),以及返回的资源是否需要缓存,客户端是否需要发送cookie等。

4.传输层TCP传输报文

位于传输层的TCP协议提供可靠的字节流服务,他为了方便传输,将大块的数据分割成以报文段为基本单位的数据包进行管理。并未他们编号。方便接收端收到报文后进行组装,还原报头信息。

为了保证可靠性传输,TCP采用三次握手来保证可靠性传输。

5.网络层IP协议查询MAC地址

IP协议把TCP分割的数据包传送给接收方。而要保证却是能够传送给对方主机还需要MAC地址,也就是物理地址。IP地址和MAC地址是一一对应关系。一个设备有且只有一个MAC地址。IP地址可以更换,MAC不会变。ARP协议就是讲IP协议转换成MAC地址的协议,利用ARP协议,找到MAC地址,当通信的双方不在同一个局域网时,还需要多次中转,才能到达目标,在中转时,通过下一个MAC地址来搜索下一个中转目标。

6.数据到达链路层

找到对方的MAC地址后,就将数据包放到链路层进行传输,封装上链路层特有的报头,然后交付给物理层,物理层通过实际的电路如双绞线进行传输。

走到数据链路层,客户端的请求发送阶段完毕。

7.服务器接受请求

服务端主机的网卡接收到数据后,驱动操作系统拿到数据,自下而上进行解包,数据包到链路层,就解析客户端在链路层封装的报头信息,提取报头信息内容,如目标MAC地址,IP地址等。

到达网络层,提取IP协议报头信息,到达传输层,解析传输层协议报头,

到达应用层,解析HTTP报头信息,获得客户端请求的资源和方法。查找到资源后。将资源返回给客户端,并返回响应报文。

响应报文中包括协议名称/协议版本,状态码,状态码描述等信息。其中常见状态码:200 表示请求资源成功。301:永久重定向,表示资源已经永久性重定向到指定位置。

8.请求成功返回相应资源
请求成功后,服务器会返回相应的HTMML文件,该文件的传输方式又会从应用层出发,自上问下传送,到达对端时,自下而上进行解析。

二、页面渲染

现代浏览器渲染页面的过程是这样的:解析HTML以构建DOM树 –> 构建渲染树 –> 布局渲染树 –> 绘制渲染树。

DOM树是由HTML文件中的标签排列组成,渲染树是在DOM树中加入CSS或HTML中的style样式而形成。渲染树只包含需要显示在页面中的DOM元素,像元素或display属性值为none的元素都不在渲染树中。

在浏览器还没接收到完整的HTML文件时,它就开始渲染页面了,在遇到外部链入的脚本标签或样式标签或图片时,会再次发送HTTP请求重复上述的步骤。在收到CSS文件后会对已经渲染的页面重新渲染,加入它们应有的样式,图片文件加载完立刻显示在相应位置。在这一过程中可能会触发页面的重绘。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值