引言
上篇文章介绍了构造函数、原型和原型链的关系,并且说明了 prototype
、[[Prototype]]
和 __proto__
之间的区别,今天这篇文章用图解的方式向大家介绍原型链及其继承方案,在介绍原型链继承的过程中讲解原型链运作机制以及属性遮蔽等知识。
建议阅读上篇文章后再来阅读本文,链接:【进阶5-1期】重新认识构造函数、原型和原型链
有什么想法或者意见都可以在评论区留言。下图是本文的思维导图,高清思维导图和更多文章请看我的 Github。
原型链
上篇文章中我们介绍了原型链的概念,即每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null
,这种关系被称为原型链(prototype chain)。
根据规范不建议直接使用 __proto__
,推荐使用 Object.getPrototypeOf()
,不过为了行文方便逻辑清晰,下面都以 __proto__
代替。
注意上面的说法,原型上的方法和属性被 继承 到新对象中,并不是被复制到新对象,我们看下面这个例子。
// 木易杨
function Foo(name) {
this.name = name;
}
Foo.prototype.getName = function() {
return this.name;
}
Foo.prototype.length = 3;
let foo = new Foo('muyiy'); // 相当于 foo.__proto__ = Foo.prototype
console.dir(foo);
复制代码
原型上的属性和方法定义在 prototype
对象上,而非对象实例本身。当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null
)。
比如调用 foo.valueOf()
会发生什么?
- 首先检查
foo
对象是否具有可用的valueOf()
方法。 - 如果没有,则检查
foo
对象的原型对象(即Foo.prototype
)是否具有可用的valueof()
方法。 - 如果没有,则检查
Foo.prototype
所指向的对象的原型对象(即Object.prototype
)是否具有可用的valueOf()
方法。这里有这个方法,于是该方法被调用。
prototype
和 __proto__
上篇文章介绍了 prototype
和 __proto__
的区别,其中原型对象 prototype
是构造函数的属性,__proto__
是每个实例上都有的属性,这两个并不一样,但 foo.__proto__
和 Foo.prototype
指向同一个对象。
这次我们再深入一点,原型链的构建是依赖于 prototype
还是 __proto__
呢?
Foo.prototype
中的 prototype
并没有构建成一条原型链,其只是指向原型链中的某一处。原型链的构建依赖于 __proto__
,如上图通过 foo.__proto__
指向 Foo.prototype
,foo.__proto__.__proto__
指向 Bichon.prototype
,如此一层一层最终链接到 null
。
可以这么理解 Foo,我是一个 constructor,我也是一个 function,我身上有着 prototype 的 reference,只要随时调用 foo = new Foo(),我就会将
foo.__proto__
指向到我的 prototype 对象。
不要使用 Bar.prototype = Foo
,因为这不会执行 Foo
的原型,而是指向函数 Foo
。 因此原型链将会回溯到 Function.prototype
而不是 Foo.prototype
,因此 method
方法将不会在 Bar 的原型链上。
// 木易杨
function Foo() {
return 'foo';
}
Foo.prototype.method = function() {
return 'method';
}
function Bar() {
return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函数
let bar = new Bar();
console.dir(bar);
bar.method(); // Uncaught TypeError: bar.method is not a function
复制代码
instanceof 原理及实现
instanceof
运算符用来检测 constructor.prototype
是否存在于参数 object
的原型链上。
// 木易杨
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
复制代码
instanceof 原理就是一层一层查找 __proto__
,如果和 constructor.prototype
相等则返回 true,如果一直没有查找成功则返回 false。
instance.[__proto__...] === instance.constructor.prototype
复制代码
知道了原理后我们来实现 instanceof,代码如下。
// 木易杨
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
// Object.prototype.__proto__ === null
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
// 测试
function C(){}
function D(){}
var o = new C();
instance_of(o, C); // true
instance_of(o, D); // false
复制代码
原型链继承
原型链继承的本质是重写原型对象,代之以一个新类型的实例。如下代码,新原型 Cat
不仅有 new Animal()
实例上的全部属性和方法,并且由于指向了 Animal
原型,所以还继承了Animal
原型上的属性和方法。
// 木易杨
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
// 这里是关键,创建 Animal 的实例,并将该实例赋值给 Cat.prototype
// 相当于 Cat.prototype.__proto__ = Animal.prototype
Cat.prototype = new Animal();
var instance = new Cat();
instance.value = 'cat'; // 创建 instance 的自身属性 value
console.log(instance.run()); // cat is runing
复制代码
原型链继承方案有以下缺点:
- 1、多个实例对引用类型的操作会被篡改
- 2、子类型的原型上的 constructor 属性被重写了
- 3、给子类型原型添加属性和方法必须在替换原型之后
- 4、创建子类型实例时无法向父类型的构造函数传参
问题 1
原型链继承方案中,原型实际上会变成另一个类型的实例,如下代码,Cat.prototype
变成了 Animal
的一个实例,所以 Animal
的实例属性 names
就变成了 Cat.prototype
的属性。
而原型属性上的引用类型值会被所有实例共享,所以多个实例对引用类型的操作会被篡改。如下代码,改变了 instance1.names
后影响了 instance2
。
// 木易杨
function Animal(){
this.names = ["cat", "dog"];
}
function Cat(){}
Cat.prototype = new Animal();
var instance1 = new Cat();
instance1.names.push("tiger");
console.log(instance1.names); // ["cat", "dog", "tiger"]
var instance2 = new Cat();
console.log(instance2.names); // ["cat", "dog", "tiger"]
复制代码
问题 2
子类型原型上的 constructor 属性被重写了,执行 Cat.prototype = new Animal()
后原型被覆盖,Cat.prototype
上丢失了 constructor 属性, Cat.prototype
指向了 Animal.prototype
,而 Animal.prototype.constructor
指向了 Animal
,所以 Cat.prototype.constructor
指向了 Animal
。
Cat.prototype = new Animal();
Cat.prototype.constructor === Animal
// true
复制代码
解决办法就是重写 Cat.prototype.constructor
属性,指向自己的构造函数 Cat
。
// 木易杨
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
// 新增,重写 Cat.prototype 的 constructor 属性,指向自己的构造函数 Cat
Cat.prototype.constructor = Cat;
复制代码
问题 3
给子类型原型添加属性和方法必须在替换原型之后,原因在第二点已经解释过了,因为子类型的原型会被覆盖。
// 木易杨
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 新增
Cat.prototype.getValue = function() {
return this.value;
}
var instance = new Cat();
instance.value = 'cat';
console.log(instance.getValue()); // cat
复制代码
属性遮蔽
改造上面的代码,在 Cat.prototype
上添加 run
方法,但是 Animal.prototype
上也有一个 run
方法,不过它不会被访问到,这种情况称为属性遮蔽 (property shadowing)。
// 木易杨
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 新增
Cat.prototype.run = function() {
return 'cat cat cat';
}
var instance = new Cat();
instance.value = 'cat';
console.log(instance.run()); // cat cat cat
复制代码
那如何访问被遮蔽的属性呢?通过 __proto__
调用原型链上的属性即可。
// 接上
console.log(instance.__proto__.__proto__.run()); // undefined is runing
复制代码
其他继承方案
原型链继承方案有很多问题,实践中很少会单独使用,日常工作中使用 ES6 Class extends(模拟原型)继承方案即可,更多更详细的继承方案可以阅读我之前写的一篇文章,欢迎拍砖。
点击阅读:JavaScript 常用八种继承方案
扩展题
有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
参考答案:点击查看
小结
- 每个对象拥有一个原型对象,通过
__proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null
,这种关系被称为原型链 - 当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(
null
)。 - 原型链的构建依赖于
__proto__
,一层一层最终链接到null
。 - instanceof 原理就是一层一层查找
__proto__
,如果和constructor.prototype
相等则返回 true,如果一直没有查找成功则返回 false。 - 原型链继承的本质是重写原型对象,代之以一个新类型的实例。
参考
进阶系列目录
- 【进阶1期】 调用堆栈
- 【进阶2期】 作用域闭包
- 【进阶3期】 this全面解析
- 【进阶4期】 深浅拷贝原理
- 【进阶5期】 原型Prototype
- 【进阶6期】 高阶函数
- 【进阶7期】 事件机制
- 【进阶8期】 Event Loop原理
- 【进阶9期】 Promise原理
- 【进阶10期】Async/Await原理
- 【进阶11期】防抖/节流原理
- 【进阶12期】模块化详解
- 【进阶13期】ES6重难点
- 【进阶14期】计算机网络概述
- 【进阶15期】浏览器渲染原理
- 【进阶16期】webpack配置
- 【进阶17期】webpack原理
- 【进阶18期】前端监控
- 【进阶19期】跨域和安全
- 【进阶20期】性能优化
- 【进阶21期】VirtualDom原理
- 【进阶22期】Diff算法
- 【进阶23期】MVVM双向绑定
- 【进阶24期】Vuex原理
- 【进阶25期】Redux原理
- 【进阶26期】路由原理
- 【进阶27期】VueRouter源码解析
- 【进阶28期】ReactRouter源码解析
交流
进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。
我是木易杨,公众号「高级前端进阶」作者,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!