🔖 studynotejavascriptecmascript

前言

和其它面向对象的语言不同——继承只存在于两个不同的类之间——,Javascript 没有真正的类的概念。它采用一种原型链的机制,通过原型对象的连接关系来表达继承:通过某个属性(__proto__)将原型对象连接成树形结构,则所谓的继承即为该树中节点与其祖先节点的血缘关系。在访问某个对象的属性时,会顺着原型对象树往上寻找目标属性,并返回第一个含有此属性的节点的对应属性值。这种继承策略带来的副产品是,可以轻易地通过修改原型对象上的属性使得所有继承它的对象都拥有此新增属性[1]

CAUTION

__proto__ 虽然被纳入了 ECMA 标准中,但目前它是不被推荐使用的,请使用 Reflect.getPrototypeOf()Reflect.setPrototypeOf() 代替。

出于方便叙述考虑,本文仍采用 __proto__ 进行演示。

原型链

在 ES6 标准里,所有的对象都有两个内置属性 constructor__proto__,其中:

  • constructor: 指向创建它的构造函数。
  • __proto__: 指向创建它的构造函数的原型对象。

构造函数还有一个内置属性 prototype 指向它的原型对象;而构造函数的原型对象的 constructor 又指向此构造函数。有点绕,可以看下面的代码示例[2]

demo1.ts 
1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
this.name = name
}
const alice = new Person('Alice')
alice.constructor === Person // => true
alice.__proto__ === Person.prototype // => true
Person.constructor === Function // => true
Person.__proto__ === Function.prototype // => true
Person.prototype.constructor === Person // => true

继承

前面提到过,Javascript 通过原型对象的连接关系来表达继承,因此我们可以修改连接关系来实现继承。比如下面的代码中,想让 Student 继承自 Person,只需要将 Student 的原型对象的 __proto__ 指向 Person 的原型对象就好了(仅用于演示,请不要投入到生产中):

demo2.ts 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name) { this.name = name }
Person.prototype.great = function () {
return `Hello, ${this.name}!`
}
function Student(grade) { this.grade = grade }
Student.prototype.showGrade = function () {
return `Grade: ${this.grade}`
}
Student.prototype.__proto__ = Person.prototype
const alice = new Student(2)
alice instanceof Student // => true
alice instanceof Person // => true
alice.great() // => 'Hello, undefined!'
alice.name = 'Alice'
alice.showGrade() // => 'Grade: 2'
alice.great() // => 'Hello, Alice!'
CAUTION

上面的修改看起来很直观,但直接操作 __proto__ 是不被推荐的,你可以使用 Reflect.setPrototypeOf() 来做修改。


此外,原型对象之间的连接还可以通过 constructorconstructor.prototype 来表达,(前文提到过,对象的 constructor 指向其构造函数)如最开始的例子中:

1
2
3
alice.__proto__ === Student.prototype // => true
Student.__proto__ === Person.prototype // => true
Person.__proto__ === Function.prototype // => true

等价于:

1
2
alice.constructor.prototype === Person.prototype // => true
Person.constructor.prototype === Function.prototype // => true

因此我们可以利用 Object.create 来创建一个以 Person.prototype 为原型对象的对象,然后将 Student.prototype 指向此对象:

inherit.ts 
1
2
3
4
5
6
7
8
9
function inherit(Child, Parent) {
const prototype = Object.create(Parent.prototype)
for (const key of Object.getOwnPropertyNames(Child.prototype)) {
if (prototype.hasOwnProperty(key)) continue
prototype[key] = Child.prototype[key]
}
prototype.constructor = Child
Child.prototype = prototype
}
TIP

注意上面代码高亮部分,将 Child.prototype 的值复制到了新的原型对象中,否则调用此继承函数后,原先定义在 Child.prototype 上的属性会丢失。

创建对象

Javascript 中创建对象有多种方式:

  • new: new Person() 创建并返回一个以 Person.constructor 作为原型对象的对象,因此返回的对象继承自 Person

  • Object.create: Object.create(__proto__): 接受一个对象 __proto__,创建并返回一个以此对象作为原型对象的对象,因此使用 Object.create(null) 可以创建一个不继承自任何对象的对象。

  • const alice = { name: 'alice' }: 等价于 const alice = new Object({ name: 'alice' })

  •  [1]: 

    其它面向对象的语言不得不修改类的实现才能获得此能力。

  •  [2]: 

    需要注意的是,构造函数如果返回一个对象,则此对象的 __proto__ 将指向 Object.prototype,如:

    1
    2
    3
    4
    5
    6
    7
    8
    function Person(name) {
    this.name = name
    return { name }
    }
    const alice = Person('alice')
    alice.constructor === Object // => true
    alice.__proto__ === Object.prototype // => true
  •  [3]: 

    Javascript 访问属性时,会沿着继承链往上找,所以即便是一个空对象,还是能够访问其祖先节点上定义的属性的。

© 2017-2025 光和尘有花满渚、有酒盈瓯

Comments