JavaScript海底两万里:原型(链)
争取一文帮自己搞清楚JavaScript中的原型(链)。
Java选手们很容易看出来,如果存在三个类User
、Admin
、Guest
,这三个类之间可以存在继承关系,像这样:
flowchart BT
Admin -->|"[[Prototype]]"| User
Guest -->|"[[Prototype]]"| User
在JavaScript中,原型继承可以用来实现对象继承的能力。
[[Prototype]]
在JavaScript中,对象有一个隐藏属性[[Prototype]]
,称为“原型”,它要么是null
,要么是另一个对象的引用。
当我们从对象中读取一个缺失的属性,JavaScript会从原型中获取该属性,这称为“原型继承”,我们后面细看。
所谓隐藏属性,指[[Prototype]]
是对象内部的,而且是隐藏的,可以说相当于Java中的private字段。
但就如同Java中可以设置getter/setter
方法一样,JavaScript中也存在设置[[Prototype]]
的方式,不过不同于Java中通过extend
关键字来实现继承,一种方法是使用__proto__
:
1 |
|
现在,如果执行console.log(rabbit.eats)
,JavaScript引擎会顺着[[Prototype]]
引用,从原型链中寻找eats
属性。
不止访问属性,当访问对象中不存在的函数,JavaScript引擎也会从原型中寻找这个函数。
如果存在这样一条原型链:
flowchart BT
ShortHair -->|"[[Prototype]]"| Cat -->|"[[Prototype]]"| Animal
当我们从ShortHair
中读取一些它不存在的内容,那么JavaScript会先从cat
中查找,然后在animal
中查找。
原型链中存在两个限制:
- 引用不能形成闭环,如果在闭环中分配
__proto__
,JavaScript会报错。 __proto__
的**值只能是对象或null
**,其他的类型都会被忽略。
与Java相同,JavaScript也是单继承的。
但是事实上__proto__
有点过时了,它是[[Prototype]]
的getter/setter
,现代JavaScript建议使用Object.getPrototypeOf/Object.setPrototypeOf
来取代__proto__
,这两个函数后面再说。
在这里需要了解一下**
getter/setter
,它们也属于对象的属性,称为访问器属性,本质上是一类用于获取和设置值的函数**,但看起来就像常规属性。
1
2
3
4
5
6
7
8
let obj = {
get propName() {
// ...
},
set propName() {
// ...
}
};于是,在读取
obj.propName
时,getter
起作用;在设置obj.propName
时,setter
起作用。比如这样一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
let user = {
name: '',
surname: '',
get fullName() {
return `${this.name} ${this.surname}`;
},
set fullName(name) {
[this.name, this.surname] = name.split(' ');
}
};
user.fullName = 'John Smith';
console.log(user.fullName); // John Smith这样我们就设置了一个“虚拟”属性
fullName
,可以对其进行读写操作。
属性具有三个属性标志:
writable
:如果为true
,则值可以被修改,否则为只读。enumerable
:如果为true
,会被在循环中列出。configurable
:如果为true
,则此属性可以被删除。而相较于普通的属性标志,访问器属性没有
value
和writable
标志,这很好理解,get
和set
的存在就是读取与设置value
,同时set
是否存在也就决定了一个属性是否writable
。
F.prototype
在上一篇中我们可以知道,通过new F()
的语法可以用构造函数创建一个新对象,而对于构造函数F
,如果为它设置属性F.prototype
的值是一个对象,那么当执行let obj = new F();
时,new
操作符会使用F.prototype
为新对象obj
设置[[Prototype]]
。
1 |
|
上面的代码中,Cat.prototype = aniamal;
的意思是当new Cat(...)
时,把新对象的[[Prototype]]
赋值为animal
。
注意,F.prototype
是F
的一个名为"prototype"
的属性,这是每个函数都有的属性,而不是代表F
的原型,并且F.prototype
的变化对已经创建出来的对象时没有作用的。
默认情况下,F.prototype
是一个只有属性构造器的对象,构造器指向函数自身。
原生的原型
"prototype"
属性在JavaScript
的底层有广泛的应用,所有的内建函数都用到了它。
不愧叫JavaScript
,在原型这方面,它的思想跟Java
的Object
类也有异曲同工之妙:
1 |
|
我们创建了一个空对象obj
,但还是可以看到成功输出了obj
的信息,那么很明显是内建的toString
函数运行的结果,但是它在哪里呢?
注意,如果用
console.log()
输出obj
,那么在命令行只会看到{}
表示空对象,只有通过alert()
才能在浏览器中看到[object Object]
弹窗。默认情况下,
toString()
方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString()
返回"[object type]
“,其中type
是对象的类型。所以,
[object Object]
就表示Object对象。
我们知道let obj = {};
与let obj = new Object();
是一样的,而new Object()
是一个构造函数的调用,Object的prototype
指向一个包含一堆方法的对象,当然也包括了toString()
,这与Java中Object作为所有类的父类的理念有所雷同,所以,当new Object()
被调用或{}
被创建,这个对象的[[Prototype]]
就会被设置为Object.prototype
,而toString()
方法正是从中获取的。其他的内建对象像Array、Date等等也都在prototype上挂载了方法。
按照规范,所有的内建原型顶端都是Object.prototype
,所以才会有“一切都是从对象继承而来”的说法。
对于基本类型,同样与Java很像,JavaScript提供了临时包装器在试图访问它们的属性时通过内建构造器String()
、Number()
、Boolean()
创建,使开发者能够操作字符串、数字、布尔值,“临时”意味着这些包装器在操作结束后就会被垃圾回收器回收。
原生的原型可以被修改,但是非常不建议这么做,因为会导致严重的冲突问题,除非需要在久JavaScript环境中实现新标准的方法,这被称为polyfilling。
原型方法
之前说过,通过obj.__proto__
设置/读取原型已经是过时的方法了,现代JavaScript中推荐通过以下方式来实现:
Object.getPrototypeOf(obj)
—— 返回对象obj
的原型Object.setPrototypeOf(obj, proto)
—— 将对象obj的原型设置为proto
此外,通过Object.create(proto, [descriptors])
可以利用规定的proto作为原型以及可选的描述属性来创建一个新对象。这个方法非常强大,因为可以为对象添加额外的属性:
1 |
|
还可以实现一种浅拷贝:
1 |
|
使用 Object.create(null)
或 {__proto__: null}
还可以创建无原型的对象,这些对象被用作字典,以存储任意(可能是用户生成的)键。
通常,对象会从 Object.prototype
继承内建的方法和 __proto__
getter/setter,会占用相应的键,且可能会导致副作用。原型为 null
时,对象才真正是空的。