在项目中经常会接触到JS中原型链相关的知识,但是迫于当时的项目进度压力,通常只能迅速地从搜索引擎/个人博客等地方汲取一些碎片化知识,不利于系统化的学习整理。最近从《Professional JavaScript for Web Developers》一书中重学JS中的对象,现在尝试通过自己的理解和体会把JS中的原型链说清楚。
要说清楚JS中的原型链,其实要先把以下三个概念理解清楚:
- 什么是JS中的构造函数?
- 什么是
prototype? - 什么是
__proto__?
什么是JS中的构造函数?
关于这个问题,可能大家想到的首先是“构造函数就是一个搭配上new关键字就能生成一个新的实例的函数”,这个描述并没有说到点子上,其实ECMAScript规范里有明确规定了满足这三点的函数可以称为构造函数:
- 函数内没有显式地创建对象
- 属性和方法都直接赋值给了this
- 没有return
JS中有许多内置的构造函数,例如Function、Object、Array、String等等,可以搭配new去生成一个新的实例,这里特别说到,所有函数(包括构造函数)都是Function的一个实例,这里可能会有很多人不理解,目前,声明一个函数主要有以下三种方式:
第一种是函数声明式:
function foo(){
// ...
}
第二种是表达式:
const foo = function(){
// ...
}
第三种是用的比较少的new Function的形式
// 在new Function中除了最后一个参数,其余参数都作为新声明函数的参数,最后一个参数作为新声明函数的函数体
const foo = new Function(arg1, arg2, ..., `return ${arg1} + ${arg2}`)
以上三种方式结果是等价的,从第三种一眼就能看出foo是Function的一个实例,但是为什么我们习惯于用前两种来创建一个函数呢?一个重要的原因是new Function的方式会执行两遍,第一次是将它当作常规ECMAScript 代码,第二次是解释传给构造函数的字符串,这会影响性能,在追求极致性能的项目中这种方式并不可取。另外一个原因是前两种创建函数的方式更加直观简洁,便于阅读、理解。
到这里可能还没发现构造函数和原型链之间有什么关系,那我们可以来看一下执行new关键字时做了些什么工作:
function new(parentFn) {
// 1.新建一个空对象
let obj = {};
// 2.将新对象的__proto__属性赋值为构造函数的prototype指向的值
obj.__proto__ = parentFn.prototype;
// 3.在新对象的作用域下执行构造函数
parentFn.call(obj);
// 4.返回这个新对象
return obj;
}
此时可以很清楚的看到,new把构造函数、__proto__和prototype三者联系了起来!
什么是JS中的原型对象?
JS中每创建一个函数,都会按照特定规则添加一个prototype属性,指向它的原型对象。特别地,原型对象是由Object构造函数按照特定规则创建的一个对象,所以原型对象中除了包含一些继承自Object属性和方法外,还有一个名为constructor的属性,指回原函数。
什么是__proto__?
在构造函数每次创建一个新的实例时,实例中都有一个名为[[prototype]]的内部属性,它指向构造函数的原型对象。特别地,[[prototype]]是一个内部属性,JS中并没有提供直接访问这个属性的方法,但是现代浏览器中的JS引擎时都会将这个属性暴露出来供开发者访问,取名为__proto__。
所以我们可以用一段代码来测试这三者的关系:
// 定义一个构造函数 Foo()
function Foo(){}
// 使用Foo创建一个实例
const fooInstance = new Foo()
// 相互之间关系
console.log(Foo.prototype) // {constructor: ƒ}
console.log(Foo.prototype.constructor === Foo) // true
console.log(fooInstance.__proto__ === Foo.prototype) // true
我们也可以将他们的关系可视化表示为: 【图】
prototype/__proto__/construct三者关系
上面已经介绍完概念,现在来用一个具体例子来看看,这里引用了知乎答主doris回答里的图:
1.先看到中间Functions一列,Foo、Object和Function都是构造函数,其中,Foo和Object也是Function的一个实例,特别地,构造函数Function本身也是一个函数。
对于蓝色部分路径:
可以很容易理解到,f1/f2是Foo的实例,f1/f2中有__proto__属性指向构造函数Foo的原型对象Foo.prototype,同时,构造函数Foo自身有prototype属性指向其原型对象Foo.prototype,原型对象Foo.prototype中有constructor属性指回构造函数Foo。
对于Object与其实例o1/o2也同理。
对于橙色部分路径:Foo与Fuction的关系
因为通过function Foo()的方式声明了构造函数Foo,所以由概念可知,Foo中有__proto__属性指向构造函数Function的原型对象Function.prototype,同时,构造函数Function有prototype属性指向其原型对象Function.prototype,原型对象中有constructor属性指回构造函数Function。
对于Object和其他一些JS内置的构造函数例如Array、Number和String等与Function的关系也同理。
对于绿色部分路径
可以发现构造函数Function有__proto__属性指向Function.prototype,同时构造函数Function的prototype属性也指向Function.prototype,那这是不是说明,Function实例化了自己?可以验证一下:
// Foo是Function的一个实例时有:
const Foo = function(){}
console.log(Foo instanceof Function) // true
console.log(Object.getPrototypeOf(Foo) === Function.prototype) // true
// 测试Function是不是Function的一个实例:
console.log(Function instanceof Function) // true
console.log(Object.getPrototypeOf(Function) === Function.prototype) // true
难道这是一个先有鸡还是先有蛋的问题?我觉得并不需要钻牛角尖,只要把Function理解为JS引擎按照ECMAScript规范生成的一个对象,并且为它赋予了上面这些特征就可以了。
对于红色部分路径:
这一部分可以这样理解,所有原型对象都是一个对象实例,都是由Object创建的(Object.prototype除外),所以这些原型对象上都会继承Object原型对象上的一些属性和方法。对于Object原型对象Object.prototype,它也是一个对象,但它不是由Object创建的,这一点从图中的Object.prototype.__proto__ === null就可以知道,可以理解为Object.prototype也是JS引擎按照ECMAScript规范生成的一个对象,并且在它上面定义了一个共用的属性和方法,例如toString、hasOwnProperty等等。
这一些就是我对JS中原型链的理解,学完了原型链,与其最相关的应用场景就是继承了,那么,下一篇我将会写一写学习JS中的继承的总结。