最近在看 JavaScript 设计模式与开发实践 ,虽然是 15 年出版的书,但书中所讲的东西对我这个小白来说,还是很前沿的。书中在正式开始讲设计模式之前,先将 JavaScript 里高级基础知识: 原型this、call、apply闭包 详细介绍了一遍,对我来说简直就是福音,打算把这些挨个写篇笔记,记录一下。

原型模式

一种用以创建对象的模式,如果要想创建一个对象,一种方法是先指定它的类型,然后通过类来创建一个对象;另一种是,不在关心对象的具体类型,通过克隆创建一个一模一样的对象。

基本规则:

  • 所有数据都是对象。
  • 得到一个对象不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型。
  • 如果对象无法响应某个请求,就会把这个请求 委托 给它的原型。

JavaScript 中的原型继承

JavaScript 同样也遵守上面的基本规则,虽然分为两种类型机制:基本类型( undefined、null、number、boolean、string、symbol ),引用类型( array、object ),除了 undefinednull (它俩没有原型),这些类型都能通过构造函数的形式( new 操作符)来创建,可以说万物皆对象。而对于这些对象,追根溯源都来自一个根对象,这个根对象就是 Object.prototype

// 对于非 Object 类型,它的原型等于它构造器的原型,而它原型的原型最终还是等于 Object.prototype
let a = 0
Object.getPrototypeOf(a) === Number.prototype // true
Object.getPrototypeOf(Number.prototype) === Object.prototype // true

function Person() {}
let b = new Person()
Object.getPrototypeOf(a) === Person.prototype // true
Object.getPrototypeOf(Person.prototype) === Object.prototype // true

// 对于 Object 类型,它的原型就等于 Object.prototype
let c = {}
Object.getPrototypeOf(c) === Object.prototype // true

// 为了避免死循环,Object.prototype 的原型指向 null
Object.getPrototypeOf(Object.prototype) === null

准确来说,并不能说对象有原型,而是对象的构造器有原型。对于“对象把请求委托给它的原型”,更好的说法是,对象把请求委托给它的构造器的原型。下面手动实现一下 new 操作,就知道它内部具体都做了哪些操作。

const objectFactory = function () {
  const obj = new Object()
  const constructor = [].shift.call(arguments) // 拿到外部传入的构造器
  obj.__proto__ = constructor.prototype // 设置 __proto__ 指向构造器的原型
  const res = constructor.apply(obj, arguments) // 将外部传入的属性通过构造器创建对象
  return typeof res === "object" ? res : obj // 确保构造器返回的总是一个对象
}

function Person(name) {
  this.name = name
}

// 下面两句代码产生相同的结果
let a = new Person("Tom")
let b = objectFactory(Person, "Tom")
a.name === b.name // true

关于 __proto__prototype ,网上大部分都是以隐式原型和显式原型相称。我感觉我无法理解这个原型的一大部分原因就是这两个叫法,着实给我整迷了 😅。而 __proto__JavaScript 给对象提供的隐藏属性,是对象与它构造器的原型联系的纽带,默认都会将 __proto__ 指向构造器的原型对象,即 constructor.prototype

在被公开了 __proto__ 的浏览器中,可以验证:

let a = {}
a.__proto__ === Object.prototype // true

原型链

对于对象无法响应的请求,对象会将这个请求委托给它的原型,如果它的原型也无法响应,那么还会接着把这个请求委托给它原型的原型…… 就这样一直追溯到原型链的源头,若还是没有,就返回 undefined ,这个追溯的过程就形成了一条原型链。

除了对象正常创建时,自继承的原型,还可以动态的修改原型,以达到继承的效果。

let obj = { name: "Tom" }

let A = function () {}
A.prototype = obj

let b = new A()
console.log(b.name) // "Tom"

上方这段代码执行的过程:

  • 首先,尝试遍历对象 b 中的所有属性,但没有找到 name 这个属性。
  • 接着,将查找 name 属性这个请求委托给对象 b 构造器的原型,由于 b.__proto__ 记录着构造器的原型,并且指向 A.prototype ,而 A.prototype 被设置成了 obj
  • 最后,在对象 obj 中查找 name 属性,找到了并返回它的值。