Object Prototypes, JavaScript's Object-orientation Approach
Contents
原型(prototype)是一个对象,一个对象对某个属性的查找可以委托到对象的原型上来。
- 原型上定义的属性和方法可以自动被其它对象访问到,它的作用和传统面向对象语言的类相似。
- 使用原型可以写出面向对象风格的 JavaScript 代码。
prototypes
继承可以实现代码复用,也有利于更好地组织代码;JavaScript 使用原型实现继承。
每个对象都可以访问到它的原型,若对象本身不包含某个属性,对该属性的查找就可以委托到它的原型上。
const person = { talk: true }
const coder = { code: true }
const lawyer = { defend: true }
console.log("talk" in person, "code" in person, "defend" in person) // true false false
Object.setPrototypeOf(person, coder) // 将 coder 设为 person 的原型
console.log("code" in person, "defend" in person) // true false
Object.setPrototypeOf(coder, lawyer)
console.log("defend" in person) // true
JavaScript 的 [[prototype]]
属性是内部属性,不能直接访问;可以用 Object.setPrototypeOf
方法将第二个参数设置成第一个参数的原型。
每一个对象都可以有一个原型,原型也可以有它自己的原型,这样就形成一条原型链(prototype chain);属性的查找可以委托到整条原型链上,直到链上所有原型都询问过才会停止。
最简单的创建对象的方式是使用对象字面量,但若要批量创建同类型对象,用对象字面量的方法繁琐而易出错。
JavaScript 支持和其它面向对象语言类似的 new
操作符,通过构造函数来创建同类型的对象,但由于 JavaScript 没有类的概念,创建出来的对象是全新的对象。
每个函数(一等对象)都有一个原型(是一个对象),该对象会自动被设为用该函数创建出来的对象实例的原型。
-
函数的原型可以通过函数的
prototype
属性访问到。 -
函数的
prototype
属性(值是一个对象)初始状态下只有一个constructor
属性,它指回该函数本身。 -
创建对象的函数实质上是构造函数。
function Person() { Person.prototype.speak = function() { return true } } const p1 = Person() // 将 Person 当成普通函数调用,并没有返回一个对象 console.log(p1 === undefined) // true const p2 = new Person() console.log(p2 && p2.speak && p2.speak()) // true
存在同名的实例属性和原型属性时,实例属性优先被选择,即实例属性会屏蔽原型属性。
-
由于每个实例都拥有自己的实例属性,对于值属性而言,这是期望的结果;但若是方法属性,同一套方法逻辑在所有实例上都有不同的副本,会造成不必要的内存开销(尽管总体而言 JavaScript 引擎会做出一些优化,但不应依赖于此),从这一点上来看,方法放原型上供所有实例共享是更好的做法。
- 但若要使用方法模拟私有变量(每个实例独享),则还是要使用实例方法。
function Person() { this.busy = false this.toggleBusy = function() { // 实例属性 return !this.busy } } Person.prototype.toggleBusy = function() { // 原型属性 return this.busy } const p = new Person() console.log(p.toggleBusy()) // true 调用到的是实例属性
对象实例和创造它的构造函数的原型(一个对象)之间的引用关系在对象实例化的时候被建立起来。
-
若构造函数的原型被整个替换,老实例由于之前建立的引用关系的缘故,依旧能访问到老原型,新原型对老实例没有影响,老实例访问不到新原型;新原型只会影响以后创建的实例。
function Person() { this.focused = true } const p1 = new Person() // 实例创建后才往原型上添加的属性,之前创建的实例也能访问到, // 因为实例对原型的引用在实例化时已经建立起来 Person.prototype.isFocused = function() { return this.focused } console.log(p1.isFocused()) // true Person.prototype = { casual: function() { return false } } console.log(p1.isFocused(), p1.casual) // true undefined const p2 = new Person() console.log(p2.casual(), p2.isFocused) // false undefined
-
可以通过对象实例的原型上的
constructor
属性访问到创建对象的构造函数。constructor
的目的是确定对象实例是基于什么(构造函数)创建出来的,它的值若被覆盖,这一信息就丢失了。
function Person() {} const p1 = new Person() console.log(typeof p1 === "object", p1 instanceof Person, p1.constructor === Person) // true true true const p2 = new p1.constructor() // 等价于 new Person(),都是对同一个构造函数的引用 console.log(p2 instanceof Person, p2 !== p1) // true true
继承
拷贝(非真继承):
function Person() {}
Person.prototype.dance = function() {}
function Coder() {}
// 需要显式拷贝每一个属性
Coder.prototype = { dance: Person.prototype.dance }
const coder = new Coder()
console.log(coder instanceof Coder, coder instanceof Object) // true true
// 教会 coder 跳舞,但他仍不是一个人
console.log(coder instanceof Person) // false
要实现继承,需要实现一条原型链,最好的办法是将一个对象实例当作另一个对象的原型:
- 子类的原型是父类的一个实例;
- 父类的实例的原型是父类的构造器的
prototype
属性; - 父类的原型又是父类的父类的一个实例(
SubClass.prototype = new SuperClass()
);- 不要用
SubClass.prototype = SuperClass.prototype
来实现继承,这样的写法的结果是子类和父类的原型是同一个(紧耦合),对子类原型的修改也会反映到父类原型上来。
- 不要用
- 如此往复。
- 原型链上的原型的属性更新在继承原型的对象实例上都能访问到(live-update)。
function Person() {}
Person.prototype.dance = function() {}
function Coder() {}
// Coder 的老原型无人引用,将被删除;Coder 老原型的 constructor 属性也由于被直接覆盖而丢失了
Coder.prototype = new Person()
const coder = new Coder()
console.log(coder.constructor) // Person
// 通过 instanceof 可以判断实例是否继承了原型链上的对象
console.log(coder instanceof Coder, coder instanceof Person, coder instanceof Object) // true true true
上述实现继承的方式会导致真实的 constructor
属性值丢失,失去对象实例是基于哪个构造函数创建的信息。
JavaScript 中,每个对象的属性都用属性描述符(property descriptor)来描述,每个属性描述符可以配置以下键:
configurable
- 设为
true
时该属性的属性描述符可以编辑,属性可以删除;设为false
则两种操作都不能进行。
- 设为
enumerable
- 设为
true
时该属性可以被for-in
遍历到。
- 设为
value
- 属性的值,默认是
undefined
。
- 属性的值,默认是
writable
- 设为
true
则可以通过赋值改变属性值。
- 设为
get
- 定义
getter
函数,在访问属性时会被调用。不能跟value
和writable
同时被定义。
- 定义
set
- 定义
setter
函数,在对属性进行赋值时被调用。不能跟value
和writable
同时被定义。
- 定义
-
可以用
Object.defineProperty
方法进行属性的配置。var o = {} o.name = "sb" o.age = 29 Object.defineProperty(o, "focused", { // 可以直接访问,但不会被 for-in 遍历到 configurable: false, enumerable: false, value: true, writable: true }) console.log("focused" in o) // true for (let prop in o) { console.log(prop) // name age }
解决继承后原始 constructor
属性丢失的问题:
function Person() {}
Person.prototype.dance = function() {}
function Coder() {}
Coder.prototype = new Person()
Object.defineProperty(Coder.prototype, "constructor", {
enumerable: false,
value: Coder, // *
writable: true
})
const coder = new Coder()
console.log(coder.constructor) // Coder
for (let prop in Coder.prototype) {
console.log(prop) // dance
}
for-in
也会遍历原型链上的可枚举属性。
JavaScript 的 instanceof
操作符作用在原型链上,它检查的是右边的操作数的 prototype 是否在左边的操作数(对象实例)的原型链上。
function Person() {}
function Coder() {}
Coder.prototype = new Person()
const coder = new Coder()
console.log(coder instanceof Coder, coder instanceof Person) // true true
coder
实例的原型链包含Person
的一个实例和Person
函数的prototype
属性,判断coder instanceof Coder
时,JavaScript 引擎检查Coder
的原型,即Person
的一个实例,是否在coder
实例的原型链上。
function Person() {}
const p = new Person()
console.log(p instanceof Person) // true
// 实例 p 的原型链在初始化时已固定,不会受到 Person.prototype 被完全替换的影响
Person.prototype = {}
console.log(p instanceof Person) // false
class
ES6 标准化了模拟基于类的继承语法;class
是语法糖,底层实现仍是基于原型继承。
class Person {
constructor(name) {
this.name = name
}
isFocused() { // 原型方法,所有实例共用
return true
}
}
var person = new Person("sb")
console.log(person instanceof Person, person.name, person.isFocused()) // true sb true
// pre-ES6 approach
function Person(name) {
this.name = name
}
Person.prototype.isFocused = function() {
return true
}
在类上还可以创建静态方法,静态方法通过类而不是实例访问。
class Person {
constructor(age) {
this.age = age
}
static sum(person1, person2) {
return person1.age + person2.age
}
}
var p1 = new Person(29)
var p2 = new Person(13)
Person.sum(p1, p2) // 42
console.log("compare" in p1) // false
// pre-ES6 approach
function Person() {}
Person.sum = function(person1, person2) {}
extends
简化了继承的写法;super
方法用于调用父类构造器。
class Person {
constructor(name) {
this.name = name
}
dance() {
return true
}
}
class Coder extends Person {
constructor(name, level) {
super(name)
this.level = level
}
isFocused() {
return true
}
}
const coder = new Coder("sb", 6)
console.log(coder instanceof Coder, coder instanceof Person) // true true
console.log(coder.dance(), coder.isFocused()) // true true