JavaScript进阶笔记(六):原型链和继承的关系

一、原型链

回忆一下什么是原型链,即每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这种关系被称为原型链(prototype chain)。

原型上的方法和属性并不是复制到新对象中。

1
2
3
4
5
6
7
8
9
function Foo(name) {
this.name = name
}
Foo.prototype.getName = function () {
return this.name
}
Foo.prototype.length = 3
let foo = new Foo('owenli')
foo

原型上的属性和方法定义在 prototype 对象上,当访问一个属性或者方法时,不仅仅在对象中查找,还会查找对象的原型的原型,一级一级向上查找,直到找到或者原型链的末尾(null)。

当调用 foo.valueOf() 时,方法的查找顺序?

  • 首先从 foo 对象中查找
  • 如果没有,检查 foo 对象的原型(Foo.prototype) 是否具有该方法。
  • 如果没有,检查 Foo.prototype 所指向的原型对象(Object.prototype)是否具有该方法。此时有该方法,被调用。

1.1 prototype__proto__

原型对象的 prototype 是构造函数的属性,__proto__ 是实例对象的属性。两个指向同一个对象。

原型链依赖 prototype 还是 __proto__ ?

通过上图发现,prototype并没有构成原型链,他只是指向原型链中的某一处。原型链依赖 __proto__

1.2 instanceof 原理及其实现

instanceof用来检测构造函数的prototype属性是否出现在对象的原型链中的任意位置。

1
2
3
4
5
6
7
8
function A (){}

function B (){}

let a = new A()

a instanceof A // true
a instanceof B // false 因为B不再原型链中。

instanceof 原理是一层层查找 __proto__, 如果 constructor.prototype相等则返回 true,如果没有查找到则返回 false。

知道了原理,模拟实现一个 instanceOf

1
2
3
4
5
6
7
8
9
10
11
function instance_of (L, R) { // L:左表达式;R:右表达式
var O = R.prototype // 取R的原型
L = L.__proto__ // 取L的隐式原型
while (true) {
if (L === null) return false
if (L === O) return true
L = L.__proto__
}
}
instance_of(a, A) // true
instance_of(a, B) // false

二、原型链继承

原型链继承的本质是重写原型对象,代之以一个新类型的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person () {
this.name = 'person'
}
Person.prototype.eat = function () {
return this.name + ' is eating'
}
function Student () {}
// 这里是关键,创建 Person 的实例,并将该实例赋值给 Student.prototype
// 相当于 Student.prototype.__proto__ == Person.prototype
Student.prototype = new Person()

var stu = new Student()
stu.name = 'owenli'
console.log(stu.eat()) // owneli is eating

原型链继承方案有以下缺点:

  1. 多个实例对引用类型的操作会被篡改
  2. 子类型的原型上的 constructor 属性被重写了
  3. 给子类型原型添加属性和方法必须在替换原型之后
  4. 创建子类型实例时无法向父类型的构造函数传参

2.1 问题1

原型链继承方案中,原型实际是另一个类型的实例。如,Student.prototype变成了 Person 的实例,所以 Person 的实例属性 names 变成了 Student.prototype 的属性。

而原型属性上的引用类型会被所有实例共享,这就导致多个实例可以同时修改。

1
2
3
4
5
6
7
8
9
10
11
12
function Person () {
this.names = ['person1', 'person2']
}
function Student () {}
Student.prototype = new Person()

var stu1 = new Student()
stu1.names.push('student1')
console.log(stu1.names) // ["person1", "person2", "student1"]

var stu2 = new Student()
console.log(stu2.names) // ["person1", "person2", "student1"]

2.2 问题二

子类原型上的 constructor 被属性被重写了。

1
2
3
Student.prototype = new Person() // 覆盖原型
// 此时, Student 的原型的constructor 被重写。
Student.prototype.constructor === Person // true

解决方法:手动重写

1
Student.prototype.constructor === Student

2.3 问题三

给子类原型添加属性和方法必须在替换原型之后,不然会被覆盖丢失。

2.4 属性遮蔽

上面的代码中,我们在 Person 的原型中添加一个 eat 方法。那么,在 Student 的原型中添加一个同名的 eat 方法。
此时返回的是 Student 中新增的方法。称这种现象为属性遮蔽。

那么如何访问被遮蔽的属性呢?通过 __proto__调用原型链上的属性即可。

小结

  • 每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这种关系被称为**原型链 **
  • 当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。
  • 原型链的构建依赖于 __proto__,一层一层最终链接到 null。
  • instanceof 原理就是一层一层查找 __proto__,如果和 constructor.prototype 相等则返回 true,如果一直没有查找成功则返回 false。
  • 原型链继承的本质是重写原型对象,代之以一个新类型的实例

其他的继承方案:JS常见的八种继承方案

三、判断数组是否为空的三种方法及其优缺点

Object.prototype.toString.call()、instanceof、Array.isArray()

3.1 Object.prototype.toString.call()

每个继承 Object 的对象都有 toString 方法,如果没有被重写,则返回 [object xxx],其中的 xxx 是对象的类型。但是除了 Object 类型之外,其他的类型的 toString 方法,会直接返回内容的字符串。所以使用 call 或者 apply 方法改变 toString 的执行上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = ['Owenli', 'laal']
arr.toString() //"Owenli,laal"
Object.prototype.toString.call(arr) //"[object Array]"

// 对于基本数据类型
Object.prototype.toString.call('owenli') //"[object String]"
Object.prototype.toString.call(1) //"[object Number]"
Object.prototype.toString.call(Symbol()) //"[object Symbol]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call({}) //"[object Object]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function() {}) //"[object Function]"

常用于判断浏览器内置对象。

3.2 instanceof

通过判断对象的原型链中是否能找到 prototype。

1
2
[] instanceof Array // true
// 注意:所有的对象类型 intanceof Object 都是 true。

3.3 Array.isArray()

该方法是在 ES5 中增加的。

1
2
3
4
5
6
7
8
9
Array.isArray([]) // true

// 不错在 `isArray` 时,可以使用一下方法代替:

if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

在检测 Array 实例时,Array.isArray 优于 instanceof 因为前者可以检测 iframes

参考

木易杨前端进阶