Mark24
JavaScript原型链
JS如何实现继承?
继承: 继承就是一个对象可以访问另外一个对象中的属性和方法。
比如对象A 有run方法 A.run ;如果B继承了A,那么B可以 B.run,就仿佛B自带这个方法一样。
不同语言实现继承的方式是不同的,其中最典型的两种:
-
基于类的设计
-
基于原型的设计
C++、Java、C#这些语言都是基于经典的类设计的设计模式,这在模式最大的特点就是提供了非此复杂的规则,并且提供了非常多的关键字比如 class、protected、private、interface等。
使用基于类的继承,如果业务复杂需要维护非常复杂的继承关系。
而JS的继承方式和其他的语言有很大差别,JS本身不提供一个class实现。(ES6提供的class是一个语法糖)。JS的继承和class没有一点关系。
JS仅仅在对象中引入了一个原型的属性就实现了语言继承机制,省去了很多复杂。
原型继承如何实现
比如有对象 A、B、C; 他们拥有如下属性
A { color, __proto__}
B { name, __proto__}
C { type, __proto__}
C可以直接访问自己的属性是无疑的,访问B对象呢?
JS的每个对象都包含了一个隐藏属性 __proto__
,我们就把隐藏的属性 __proto__
称之为 该对象的原型(prototype), __proto__
指向内存中另外一个对象,我们把 __proto__
指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。
比如 让C对象的原型指向B对象
对象C访问B中的属性name时,JS引擎会先从对象C中查找,但是没有查找到,接下来JS引擎继续在原型对象B中查找,因为B中包含name属性,那么JS引擎就直接返回B中name属性,虽然C和B是两个不同的对象,但是使用的时候,B的属性看上去就像是C属性意义。
同样的方式,B也是一个对象,也有自己的 __proto__
比如他属性指向内存中的另一个对象A。
C.name
、C.color
给人的感觉是C本身的属性,但是实际上这些属性都是位于原型对象上,我们把这种查找属性的路径称为 “原型链”,他像一个链条一样,将几个原型对象连接了起来。
不要搞混:原型链、作用域链
-
作用域链:沿着函数的作用域一级一级来查找变量
-
原型链:沿着对象的原型一级一级来查找属性
继承就是一个对象可以访问另外一个对象中的属性和方法,在JS中,我们可以通过原型和原型链的方式来实现了继承特性。
var animal = {
type: "Default",
color: "Default",
getInfo: function() {
return `Type is:${this.type},color is ${this.color}`
}
}
var dog = {
type: "Dog",
color: "Black"
}
这段代码中dog如果想要继承animal:
dog.__proto__ = animal
设置后可以使用 dog.getInfo()
但是__proto__
本质上是隐藏属性,实际项目中,不应该直接访问或者修改。主要原因:
-
这是隐藏属性,并不是标准定义
-
使用这个属性会造成严重的性能问题
这只是方便理解概念。
如何应该正确的设置原型对象? 应该区使用构造函数。
构造函数是怎么创建对象的?
函数内部,通过 this 设置属性,再结合关键字 new 就可以创建对象。
function DogFactory(type,color) {
this.type = type;
this.color = color;
}
var dog = new DogFactory("Dog","Black")
这几句在V8引擎里行为可以模拟为:
var dog = {}
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog.'Dog','black')
构造函数怎么实现继承
一个函数其实有几个隐藏属性,比如 code
,name
,还有 prototype
。每个函数对象都有一个公开的 prototype
属性,当你将这个函数作为构造函数来创建一个新的对象的时候,新创建对象的原型对象就指向了该函数的prototype
属性。
如果你只是正常调用该函数,那么prototype
属性将不起作用。
function DogFactory(type,color) {
this.type = type;
this.color = color;
}
var dog1 = new DogFactory('Dog','Black');
var dog2 = new DogFactory('Dog','Black');
var dog3 = new DogFactory('Dog','Black');
代码中,三个dog对象的原型对象都指向了 prototype。
function DogFactory(type,color) {
this.type = type;
this.color = color;
}
// 新增继承属性
DogFactory.prototype.constant_temperature = 1;
var dog1 = new DogFactory('Dog','Black');
var dog2 = new DogFactory('Dog','Black');
var dog3 = new DogFactory('Dog','Black');
在DogFactory.prototype
中添加属性,就会被继承。这就是继承正确方式。
总结
-
构造函数,使用this绑定属性
-
构造函数本质上是生成一个对象,再 通过 call调用执行,绑定对象属性
-
函数一旦被用作构造函数,隐藏属性 prototype对象发挥作用,这是一个储存变量、方法的对象
-
新建的实例对象的
__proto__
指向构造函数.prototype
这样实现了继承。
示意图
(图:最小原型结构示意图)
一段关于new的历史
JavaScript的名字来源于蹭JavaScript的热度。在语法层上,加入了Java中的new。
原型链各种示意图
// 可以根据log打印出真实的对应关系
function Person() {
}
p = new Person()
console.log(p.__proto__=== Person.prototype)// true
console.log(p.__proto__.__proto__=== Object.prototype) // true
console.log(p.__proto__.__proto__.__proto__=== null) // true
我又整理了一份
instanceof的原理
> function Animal(){}
undefined
>
> cat = new Animal()
Animal {}
>
> cat instanceof Animal
true
>
instanceof
的原理就是
cat.__proto__ === Animal.prototype
看对实例对象__proto__
和目标构造函数的prototype
指向是否一致。
由于存在 原型对象,只要是一个对象就有 __proto__
属性,所以可以写成递归的模式,顺着原型链比较,只要比较正确就认为是:
// 通过判断对象的原型链上是否存在prototype
function myInstanceof(left,right) {
// 获得类型的原型
let prototype = right.prototype;
// 获得对象的原型
left = left.__proto__;
// 判断对象的类型是否等于类型的原型
while (true) {
if(left === null) { return false} // null是因为最末端的就是null
if (prototype === left) { return true }
left = left.__proto__;
}
}