深入分析原型链的继承

前言

今天我就要跟你这个原型链决一死战!

创建对象

首先我们来介绍一下对象:

介绍对象

1). 属性类型

(a) 数据属性
  • [[Configurable]] 能否通过delete删除属性从而重新定义属性,默认true
  • [[Enumeralbe]] 能否通过for-in循环返回属性,默认true
  • [[Writable]] 能否修改属性的值。默认值为true
  • [[Value]] 包含这个属性的数据值。默认值为Undefined
1
2
3
4
5
6
var person = {}
Object.defineProperty(person, "name", {
writable: false,
configurable: false,
value: "PetnaKanojo"
})
(b) 访问器属性

使用访问器属性的常见方式是设置一个属性的值会导致其他属性发生变化

  • [[Configurable]] 能否通过delete删除属性从而重新定义属性
  • [[Enumerable]] 能否通过for-in循环返回属性
  • [[Get]] 在读取属性时调用的函数
  • [[Set]] 在写入属性时调用的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var book = {
_year: 2004,
edition: 1
}
Object.defineProperty(book, "year", { // 这里是访问器属性
get: function() {
return this._year
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue
this._edition += newValue - 2004
}
}
})
book.year = 2005
alert(book.edition) // 2

⚠️:可通过Object.defineProperties()来定义多个属性,可以是数据属性也可以是访问器属性。

2)读取特性

使用Object.getOwnPropertyDescription()方法来取得给定属性的描述符。根据属性类型的不同,返回的对象属性也不同。

属性类型 返回的对象属性
访问器属性 configurable, enumerable, get, set
数据属性 configurable, enumerable, writable, value

一、对象分类

JS中,可以将对象分为“内部对象”、“宿主对象”和“自定义对象”三种。

  • 内部对象

    ​ js中的内部对象包括Array、Boolean、Date、Function、Global、Math、Number、Object、RegExp、String以及各种错误类对象,包括Error、EvalError、RangeError、ReferenceError、SyntaxError和TypeError。

    ​ 其中Global和Math这两个对象又被称为“内置对象”,这两个对象在脚本程序初始化时被创建,不必实例化这两个对象。

  • 宿主对象

    ​ 宿主对象就是执行JS脚本的环境提供的对象。对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,所以又称为浏览器对象,如IE、Firefox等浏览器提供的对象。不同的浏览器提供的宿主对象可能不同,即使提供的对象相同,其实现方式也大相径庭!这会带来浏览器兼容问题,增加开发难度。

    ​ 浏览器对象有很多,如Window和Document,Element,form,image,等等。

  • 自定义对象

    ​ 顾名思义,就是开发人员自己定义的对象。JS允许使用自定义对象,使JS应用及功能得到扩充

二、工厂模式

这种就是内部创建一个对象,然后给对象添加属性。

1
2
3
4
5
6
7
8
9
function createPerson(name,age) {
var o = new Object()
o.name = name
o.age = age
o.sayName = function() {
console.log(this.name)
}
return o
}

三、构造函数模式

1
2
3
4
5
6
7
8
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function() {
console.log(this.name)
}
}
var personA = new Person("wwh", 3)

按照国际惯例,构造函数始终以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

要创建一个Person的新实例,必须使用new操作符。

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this指向了这个对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

所以这个new的实现应该是这样的。

1
2
3
4
5
6
function _new(constructor, ...params) {
const object = {}
object.__proto__ = constructor.prototype
const reObject = constructor.apply(object, params)
return (typeof reObject === 'object' && reObject !== null) ? reObject : object
}

这里又涉及了一些构造函数的骚操作。

1. 构造函数当作函数

任何函数只要通过new操作符来调用,都可以作为构造函数。而任何函数不通过new操作符来调用,也都只是一个普通函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 当作构造函数使用
var person = new Person("test", 33)
person.sayName() // "test"
// 作为普通函数调用
Person("Alice", 3)
window.sayName() // "Alice"
// 在另一个对象的作用域中调用
var o = new Object()
Person.call(o, "gg", 10)
o.sayName() // "gg"

2. 构造函数的问题

但是通过这样的方式构建的Person其实存在浪费。

1
person1.sayName == person2.sayName //false

我们应该通过把函数定义转移到构造函数外部来解决创建两个完成了同样任务的Function实例。

1
2
3
4
5
6
7
8
function Person(name, age) {
this.name = name
this.age = age
this.sayName = sayName
}
function sayName() {
console.log(this.name)
}

四、 原型模式

创建的每个函数都有一个prototype属性,指向一个对象。而这个对象的用途是包括可以由特定类型的所有实例共享的属性和方法。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始,如果找到了,返回。如果没有找到,会继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。实例属性会覆盖原型属性。

1
2
3
4
5
6
7
8
function A() {}
A.prototype.name = "A"
A.prototype.age = 3
A.prototype.sayName = function() {
console.log(this.name)
}
let a = new A()
a.sayName() // "A"

可通过delete删除实例属性

1
2
3
4
5
6
7
8
9
10
11
12
function A() {
this.name = "insideA"
}
A.prototype.name = "A"
A.prototype.sayName = function() {
console.log(this.name)
}
let a = new A()
a.sayName() // "insideA"
delete a.name
a.sayName() // "A"

五、组合使用构造函数模式和原型模式👏

使用非常广泛,可以说这是用来定义引用类型的一种默认模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name)
}
}
var person1 = new Person("Bob", 10)
var person2 = new Person("Alice", 12)
person1.sayName == person2.sayName // true
person1.name == person2.name // false

六、动态原型模式

为了把所有信息都封装在构造函数,而通过在构造函数中初始化原型,又同时保持了使用构造函数和原型的优点。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age) {
this.name = name
this.age = age
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
console.log(this.name)
}
}
}
var person1 = new Person("Bob", 10)
var person2 = new Person("Alice", 12)
person1.sayName == person2.sayName // true

七、寄生构造函数模式

这个模式是为了为对象创建构造函数。比如像创建一个具有额外方法的特殊数组,但是不能直接修改Array的构造函数。

1
2
3
4
5
6
7
8
function SpecialArray() {
var values = new Array()
values.push.apply(values, arguments)
values.toPipedString = function() {
return this.join("|")
}
return values
}

八、稳妥构造函数模式

原型链

首先我要上张图

image-20190308192224710

一、借用构造函数

1. 原理

就是在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可以通过apply和call在将来新创建的对象上执行构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType() {
this.colors = ["red"]
}
function SubType() {
SuperType.call(this)
// 在未来将要创建的SubType实例的环境下调用了SuperType的构造函数。
// 这样就会在新的SubType对象上执行SuperType()函数中定义的所有对象初始化代码
}
var instance1 = new SubType()
instance1.colors.push("black")
alert(instance1.colors) // ["red", "black"]
var instance2 = new SubType()
alert(instance2.colors) // ["red"]

2. 问题

方法在构造函数中定义,这样函数就不能复用了。而且SuperType在原型中定义的方法不能在子类型中不可见。这样的话那么SuperType的函数也必须定义在内部。这样太不合适啦!

二、组合继承(用得最多!)

1. 原理

将原型链和借用构造函数的技术组合。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Super(name) {
this.name = name
this.colors = ["red"]
}
Super.prototype.sayName = function() {
console.log(this.name)
}
function Sub(name, age) {
// 继承属性
Super.call(this, name)
this.age = age
}
// 继承方法
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
Sub.prototype.sayAge = function() {
console.log(this.age)
}
1) 继承属性
  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this指向了这个对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
2) 继承方法

为了理解继承方法的原理:

1
let test = new Super("test")

image-20190309114941257

1
test.__proto__ = Super.prototype

因此执行结果为:

1
2
3
4
5
6
7
8
9
Sub.prototype = new Super() = test
// test.__protot__ = Super.prototype
// test 上还有继承属性
// Sub.prototype.__proto__ = Super.prototype
// Sub.prototype.constructor.prototype = Super.prototype
Sub.prototype.constructor = Sub
// 此时 Sub.prototype.__proto__还是 Super.prototype
// 但是Sub.prototype就空出来了,而且Sub.prototype.constructor还是等于Sub,通过Sub.prototype.construtor.prototype继承Super的prototype
3) 辨别

使用instanceofisPropertyOf()来进行识别。

1
2
3
4
5
let c = new Sub()
c instanceof Super // true
c instanceof Sub // true
Super.prototype.isPropertyOf(c) // true
Sub.prototype.isPropertyOf(c) // true

2. 问题

调用了两次Super构造函数。

一次是在内部,使用call方式继承属性,第二次是在外部,继承prototype上的方法的时候。

三、原型式继承

原理

借助原型可以基于已有的对象创建新对象,而且不必因此创建自定义类型

1
2
3
4
5
function object(o) {
function F() {}
F.prototype = o
return new F()
}

但是这是一个浅拷贝。当o 中有引用项时,会对原属性进行修改。

image-20190309153238172

四、寄生式继承

原理

与寄生构造函数和工厂模式类似,即创建一个仅用于封装过程的函数,在内部以某种方式增强对象。

1
2
3
4
5
6
7
function createAnother(original) {
var clone = object(original)
clone.sayHi = function() {
console.log("hi")
}
return clone
}

image-20190309153341961

五、寄生组合式继承

1. 对组合继承的缺点思考

组合继承最大的问题就是会调用两次Super类型构造函数。

image-20190309154814965

这个的意思就是每次使用组合继承创建对象的时候,其实都在原型和实例中创建了两组Super的实例属性,但是实例属性的优先级高于原型中的属性。

来理解一下两次调用发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Super(name) {
this.name = name
this.colors = ["red"]
}
Super.prototype.sayName = function() {
console.log(this.name)
}
function Sub(name, age) {
// 继承属性
Super.call(this, name) // 第二次调用
this.age = age
}
// 继承方法
Sub.prototype = new Super() // 第一次调用
Sub.prototype.constructor = Sub
Sub.prototype.sayAge = function() {
console.log(this.age)
}
  • 第一次调用的时候,利用new Super() 创建的时候,实际上 Sub.prototype 上发生了这样的事情(好糟糕!)

  • 将构造函数的作用域赋给新对象,在新对象上面执行构造函数的代码(创建属性)

    1
    2
    3
    4
    5
    Sub.prototype = new Super()
    // Sub.prototype.name = name
    // Sub.prototype.colors = ["red"]
    // Sub.prototype.__proto__ = Super.prototype // 获取原型链上的函数
  • 而当第二次调用的时候,利用new Sub() 创建一个对象的时候,在Sub()构造函数内也调用了一次Super.call(this, name)

  • 这个时候实际上在Sub内部又跑了一次Super()构造函数内的代码,而这样就使得Sub实例内又得到了一次namecolors属性!!!!

    1
    2
    3
    4
    let c = new Sub("c")
    // 由于Super.call(this, "c")
    // c.name = "c"
    // c.colors = ["red"]

⚠️⚠️ 这样的话就导致了Super的实例属性在Sub上复制了两次!!!!

image-20190309155740703

因此我们需要寻找一个新的解决方案!那就是寄生组合式继承!🎉🎉

2. 原理

借用构造函数来继承属性,通过原型链的混成形式来继承方法。我们不需为了指定子类型的原型来调用超类型的构造函数,我们需要的仅仅是超类型的原型的一个副本。

1
2
3
4
5
function inheritPrototype(sub, super) {
var prototype = object(super.prototype) // 创建超类型原型的副本
prototype.constructor = sub // 增强对象
sub.prototype = prototype // 指定对象
}

原型链间的验证

1
2
3
4
5
6
7
8
9
function Cat(name, color, age) {
this.name = name
this.age = (age === undefined) ? 0 : age
this.color = color
}
Cat.prototype.meow = function() {
console.log(`I am ${this.name}, my age is ${this.age} and my color is ${this.color}`)
}
const catA = new Cat("catA", "black", 2)

1. 实例对象.constructor

1
2
alert(catA.constructor === Cat) // true
alert(catA.__prototype__ === Cat.prototype) // true

2. 实例对象 instanceof 原型对象

验证原型对象与实例对象之间的关系

1
alert(catA instanceof Cat) // true

3. prototype对象.isPrototypeOf (实例)

用来验证某个prototype对象和某个实例之间的关系

1
2
3
4
5
6
7
8
Cat.prototype.isPrototypeOf(catA)
// true
Cat.prototype.isPrototypeOf(catA)
// true
Object.prototype.isPrototypeOf(Cat)
// true
Object.prototype.isPrototypeOf(catA)
// true

image-20190309210757412

我们还能使用Object.getPropertyOf()来返回 __proto__ 的值(对象)

4. hasOwnProperty(“属性名”)

用来判断某一个属性到底是本地属性还是继承自prototype的属性

1
2
3
4
5
6
catA.hasOwnProperty("meow") // false
catA.hasOwnProperty("name") // true
catA.__proto__.hasOwnProperty("meow") // true
Cat.hasOwnProperty("name") // true
Cat.hasOwnProperty("meow") // false
Cat.prototype.hasOwnProperty("meow") // true

实现代码:

1
2
3
4
5
6
7
8
9
function hasOwnProperty(obj, prop) {
const proto = obj.__proto__ || obj.constructor.prototype
const propInObj = (prop in obj)
const propInObjPrototype =
(!(prop in proto) || (proto[prop] !== obj[prop]))
// 前一个表示prop不能再proto中
// 后一个表示可以在proto中但obj跟proto的prop不是同一个
return propInObj && propInObjPrototype
}

5. “属性名” in 实例

1
2
3
4
5
'name' in catA // true
'meow' in catA // true
'name' in Cat // true
'meow' in Cat // false 不在Cat中,在Cat.prototype中哦
'meow' in Cat.prototype // true

此时通过遍历一遍可以得到原因:
image-20190309210644487

后记

这门语言的设计真是有点糟糕

习题练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 实现一个Human,输入输出如下
Human('Peter')
// I am Peter
Human('Peter').eat('apple')
// I am Peter
// Start to eat apple
Human('Peter').rest(10).eat('apple')
// I am Peter
// Starting rest 10s
// (等待十秒)
// Start to eat apple
Human('peter').go('home').rest(10).eat('apple')
// I am Peter
// Start to go home
// Starting rest 10s
// (等待十秒)
// Start to eat apple
Human('Peter').eat('apple').rest(10).go('home')
// I am Peter
// Start to eat apple
// Starting rest 10s
// (等待十秒)
// Start to go home