原型(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

要实现继承,需要实现一条原型链,最好的办法是将一个对象实例当作另一个对象的原型

  1. 子类的原型是父类的一个实例;
  2. 父类的实例的原型是父类的构造器的 prototype 属性;
  3. 父类的原型又是父类的父类的一个实例(SubClass.prototype = new SuperClass());
    • 不要用 SubClass.prototype = SuperClass.prototype 来实现继承,这样的写法的结果是子类和父类的原型是同一个(紧耦合),对子类原型的修改也会反映到父类原型上来。
  4. 如此往复。
  • 原型链上的原型的属性更新在继承原型的对象实例上都能访问到(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)来描述,每个属性描述符可以配置以下键:

  1. configurable
    • 设为 true 时该属性的属性描述符可以编辑,属性可以删除;设为 false 则两种操作都不能进行。
  2. enumerable
    • 设为 true 时该属性可以被 for-in 遍历到。
  3. value
    • 属性的值,默认是 undefined
  4. writable
    • 设为 true 则可以通过赋值改变属性值。
  5. get
    • 定义 getter 函数,在访问属性时会被调用。不能跟 valuewritable 同时被定义。
  6. set
    • 定义 setter 函数,在对属性进行赋值时被调用。不能跟 valuewritable 同时被定义。
  • 可以用 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