继续啃高程中... 这次是关于 JS 原型链的问题,和在看某乎文章时提及的定时器的工作原理问题。
关键词:原型链、闭包、变量作用域(ES6语法)、IIFE、定时器原理...
原型链
原理
构造函数、原型与实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
下面这个例子可以辅助理解这一问题:
// 构造函数
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
// 生成实例对象
var person1 = new Person("John", "male");
var person2 = new Person("Ada", "female");
console.log(person1.name, person2.gender); // John Female
为了生成 person1
和 person2
两个实例对象,首先要创建构造函数。
与普通函数不同是,只有通过 new
创造实例对象的函数才为构造函数。
上例创建的构造函数不仅有需要定义的两个属性:name
和 gender
,还需具有实例间可共享的 prototype
属性(原型)。
而此时两个实例对象都具有一个特别的 __proto__
属性,用于链接构造函数的 prototype
共享属性。
// 构造函数
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
// 共享属性 prototype
console.log(Person.prototype); // { }
Person.prototype = { country: "China" };
console.log(Person.prototype); // { country: "China" }
// 生成实例对象
var person1 = new Person("John", "male");
var person2 = new Person("Ada", "female");
console.log(person1.country, person2.country); // China China
上面继续添加了 Person
的 prototype
属性,重新生成 person1
和 person2
两个实例对象后,发现两个实例对象都拥有一个共同的属性 country
。
所以总结上述,可得出以下结果:
person1.__proto__ == Person.prototype; // true
person1.__proto__.__proto__ === Person.prototype.__proto__ // true
Person.prototype.__proto__ === Object.prototype // true
person1.__proto__.__proto__ === Object.prototype // true
如果想要知道实例对象的属性是否属于 prototype
,可以使用 hasOwnProperty()
方法检查,用法见下例:
person1.hasOwnProperty('name'); //true
person2.hasOwnProperty('gender'); //true
person1.hasOwnProperty('country'); //false
从上例可看出,country
不是 person1
的自有属性,而是用过原型链向上级(Person.prototype
)查找得到的,也就是继承的属性。
需要注意的是,hasOwnProperty()
方法则是由最上层原型链(Object.prototype
)查找得到的。
继承
普通原型链继承有几大缺点:
- 原型方法代码必须放在替换原型的语句之后
- 不能使用对象字面量创建原型方法,这样会改写原型链
- 当只想给实例定义的属性,将会变成原型的属性,继承的属性会被子类实例共享
- 在创建子类型的实例时,不能向超类型的构造函数中传递函数
下面是几种原型链的继承方法:
-
借用构造函数:也称经典继承。在子类型构造函数的内部调用超类型构造函数。 (
call()
,apply()
)这种方法解决了原型链的两个主要的问题,为了确保SuperType
构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。function SuperType(name){ this.colors = ["red", "blue", "green"]; this.name = name; } function SubType(){ //继承了 SuperType 同时还传递了参数 SuperType.call(this, "Nicholas"); //注意引入方式 this.age = 29; } var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new SubType(); alert(instance2.colors); //"red,blue,green" alert(instance2.name); alert(instance2.age);
-
组合继承:也称伪经典继承。将原型链和借用构造函数糅合起来。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ //继承属性 SuperType.call(this, name); this.age = age; } //继承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Nicholas"; instance1.sayAge(); //29 var instance2 = new SubType("Greg", 27); alert(instance2.colors); //"red,blue,green" instance2.sayName(); //"Greg"; instance2.sayAge(); //27
參考鏈接:
一道面试题
在某乎上看到了一篇文章,代码如下:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
// 认为的结果 实际的结果
// 5 5
// 0 5
// 1 5
// 2 5
// 3 5
// 4 5
当看到这道题的时候,认为的结果是:5->0->1->2->3->4
,但实际琢磨完后却发现答案并不是,而是:5 ->5/5/5/5/5
。(首先输出5,其次同时输出五个5。)
首先要清楚的是,先输出的是 console.log(new Date, i);
。因为 for...
中设置了一个1秒钟的定时器,并会在随后输出。
其次在 for
循环声明的五个setTimeout()
都有对变量 i
的引用,而不是拷贝。(这里的引用不是引用类型的引用,而是 i
作为函数作用域链的一个变量,由于闭包造成的)
因为5个 setTimeout()
都涉及到延迟执行的情况,所以当主线程执行完后,Timeout这些回调依次执行(队列:FIFO),此时 i
的值已经为5。
关于闭包的拓展阅读,请看此篇。
关于定时器的工作原理,详读此篇。
那如何稍作改变,输出 5->0->1->2->3->4
呢?
-
IIFE(声明即执行的函数表达式)
for (var i = 0; i < 5; i++) { (function(j) { // j = i setTimeout(function() { console.log(new Date, j); }, 1000); })(i); } console.log(new Date, i);
-
利用按值传递,拆分函数
var output = function (i) { setTimeout(function() { console.log(new Date, i); }, 1000); }; for (var i = 0; i < 5; i++) { output(i); // i 从此处复制并传递 } console.log(new Date, i);
-
可使用ES6语法,
let
替代var
,但此方法并不全对for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(new Date, i); }, 1000); } // 之后 i 值后将不存在 console.log(new Date, i); //无法执行,将会报错
那再做一些改变,输出 0->1->2->3->4->5
呢?
最好还是使用 ES6 的 Promise 语法实现
const tasks = [];
for (var i = 0; i < 5; i++) { // 这里 i 的声明不能改成 let,如果要改该怎么做?
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, j);
resolve(); // 这里一定要 resolve,否则代码不会按预期 work
}, 1000 * j); // 定时器的超时时间逐步增加
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000); // 注意这里只需要把超时设置为 1 秒
});
下例是更加简洁的代码:
const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve();
}, 1000 * i);
});
// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000);
});
參考理解: