一、prototypes
Prototype 是一个对象,其中定义的属性和功能可以自动被其他对象访问。Prototype 可以发挥类似于传统的 OO 语言中类的作用,事实上 JavaScript 中的 prototype 主要用途就是编写 OO 形式的代码。
在 JavaScript 中,对象表示一系列已命名的属性及其属性值的合集。1
2
3
4
5let obj = {
prop1: 1, // 属性值为基本类型
prop2: function () {}, // 属性值为函数
prop3: {} // 属性值为另一个对象
}
关联到某个对象上的属性可以很容易地被修改或删除。1
2
3
4
5
6
7
8
9
10
11
12let obj = {
prop1: 1, // 属性值为基本类型
prop2: function () {}, // 属性值为函数
prop3: {} // 属性值为另一个对象
}
obj.prop1 = [] // 将 prop1 属性的值改为其他类型
delete obj.prop2 // 删除某个属性
obj.prop4 = "Hello" // 添加一个新的属性
console.log(obj)
// { prop1: [], prop3: {}, prop4: 'Hello' }
每个对象都可以拥有一个 prototype 的引用,在对象本身不包含某个属性时,可以由 prototype 对象去寻找该属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const yoshi = { skulk: true }
const hattori = { sneak: true }
const kuma = { creep: true }
console.log("skulk" in yoshi) // true
console.log("sneak" in yoshi) // false
console.log("creep" in yoshi) // false
// 通过 setPrototypeOf 方法将 hattori 设置为
// yoshi 对象的 prototype,yoshi 此时可以访问 sneak 属性
Object.setPrototypeOf(yoshi, hattori)
console.log("sneak" in yoshi) // true
console.log("creep" in yoshi) // false
// 将 kuma 设置为 hattori 对象的 prototype,
// yoshi 此时可以访问 creep 属性
Object.setPrototypeOf(hattori, kuma)
console.log("sneak" in yoshi) // true
console.log("creep" in yoshi) // true
prototype 与对象构建
JavaScript 中,最简单的创建新对象的语法如下:1
2
3
4
5
6const warrior = {}
warrior.name = 'Saito'
warrior.occupation = 'marksman'
console.log(warrior)
// { name: 'Saito', occupation: 'marksman' }
对于有面向对象编程背景的人来说,上述方式看上去缺少了很多东西,比如没有一个用来初始化对象的类构造器。
此外如果需要同时创建多个相同类型的对象,手动地一个一个为对象关联属性则工作量太大且容易出问题。
和其他常见的 OO 语言类似,JavaScript 也是使用 new
关键字通过构造器初始化对象。只不过该过程中被未出现类的定义,new
关键字实际上调用的是构造器函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Ninja() {}
// 每个函数都包含一个内置的 prototype 对象,可以被随意修改
Ninja.prototype.swingSword = function () {
return true
}
// 作为函数调用 Ninja(),不会创建任何对象
const ninja1 = Ninja()
console.log(ninja1) // undefined
// 作为构造器调用,一个新的对象被创建且作为构造器函数的上下文(this),
// 构造器函数的 prototype 成为新创建对象的 prototype
const ninja2 = new Ninja()
console.log(ninja2) // Ninja {}
console.log(ninja2.swingSword()) // true
当某个函数作为构造器函数使用时,构造器函数的 prototype 将成为新创建对象的 prototype。
在上面的例子中,用 swingSword
方法扩展了 Ninja.prototype
,当 ninja2
对象被创建后,其 prototype 就被设置成 Ninja 的 prototype。
因此,当后面我们访问 ninja2
对象的 swingSword
属性时,对于属性值的搜索最终传递给 Ninja
的 prototype 对象。
此外,所有通过 Ninja
构造器创建的对象都可以访问 swingSword
方法。
实例属性
当函数通过 new
关键字作为构造器调用时,其本身即成为新创建对象的上下文。除了通过 prototype 暴露属性外,还可以使用 this
关键字在构造器函数中初始化对象实例。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function Ninja() {
this.swung = false
// instance method
this.swingSword = function () {
return !this.swung
}
}
// prototype method
Ninja.prototype.swingSword = function () {
return this.swung
}
const ninja = new Ninja()
console.log(ninja.swingSword()) // true
上面代码的执行结果表明,实例方法会覆盖同名的 prototype 方法。
在构造器函数中,this
关键字指向通过 new
创建的对象。因此构造器中添加的属性会直接在新的 ninja
对象中创建。
当我们需要访问 ninja
对象的 swingSword
属性时,本就没有必要再从 prototype 中搜索。ninja
对象本身已经有了通过构造器创建的同名的实例属性。
简单来说,每个对象都包含自己版本的通过构造器创建的属性,同时也都能够访问 prototype 的属性(名字相同时实例属性优先)。
Side effects
1 | // 定义一个构造器函数,具有 swung 属性 |
从上面代码中可以看出,构造器函数的 prototype 可以随意被替换,但之前已经生成的对象实例仍指向旧的 prototype。
Object typing
1 | function Ninja() {} |
instanceof
操作符可以帮助我们确定某个对象实例是否由某个特定的构造器创建。此外,还可以借助能够被所有实例访问的 constructor
属性,因为该属性一定指向原始的构造器函数。
又因为 constructor
属性是对原始构造器的引用,它因此也可以用来实例化新的对象。
二、继承
通过 prototype 实现继承
1 | function Person() {} |
当我们通过 ninja
对象访问 dance
方法时,JavaScript 运行时首先试着检查 ninja
对象本身。ninja
本身并不包含 dance
属性,因此 ninja
对象的 prototype(即 person
对象)继续被搜索。person
对象也不包含 dance
属性,最终 person
对象的 prototype 被搜索检查,dance
方法被找到。
这就是 JavaScript 中实现继承的方式。
覆盖 constructor 属性引发的问题
在上面的代码中,通过创建一个新的 Person
对象作为 Ninja
构造器的 prototype,令 Ninja
成为 Person
的子类。这种方式会导致原始的 Ninja prototype 被替换,即丢失 constructor
属性。
而 constructor 属性对于判断对象的起源非常重要。针对这个问题,可以使用 Object.defineProperty
方法为新的 Ninja.prototype
添加 constructor
属性,将其值设置为 Ninja
:1
2
3
4
5
6
7
8
9
10
11
12
13
14function Person() {}
Person.prototype.dance = function() {}
function Ninja() {}
Ninja.prototype = new Person()
Object.defineProperty(Ninja.prototype, "constructor", {
enumerable: false,
value: Ninja,
writable: true
})
var ninja = new Ninja()
console.log(ninja.constructor === Ninja) // true
三、ES6 中的 class
ES6 中引入了一个新的 class
关键字,为创建对象、实现继承提供了一种更为优雅的方式,不必再自己通过 prototype 手动实现。1
2
3
4
5
6
7
8
9
10
11
12
13class Ninja{
constructor(name) {
this.name = name
}
swingSword() {
return true
}
}
var ninja = new Ninja("Yoshi")
console.log(ninja instanceof Ninja) // true
console.log(ninja.name) // Yoshi
console.log(ninja.swingSword()) // true
class
关键字实际上是一种语法糖,上述代码等同于 ES5 中的如下代码:1
2
3
4
5
6
7
8
9
10
11
12function Ninja(name) {
this.name = name
}
Ninja.prototype.swingSword = function() {
return true
}
var ninja = new Ninja('Yoshi')
console.log(ninja instanceof Ninja) // true
console.log(ninja.name) // Yoshi
console.log(ninja.swingSword()) // true
static methods
1 | class Ninja{ |
compare
这种 static methods 是类级别的代码,在 ES6 之前的代码中,可以这样实现:1
2function Ninja() {}
Ninja.compare = function(ninja1, ninja2) { ... }
继承
1 | class Person { |