最近在看 JavaScript 设计模式与开发实践 ,虽然是 15 年出版的书,但书中所讲的东西对我这个小白来说,还是很前沿的。书中在正式开始讲设计模式之前,先将 JavaScript 里高级基础知识: 原型 、this、call、apply 、闭包 详细介绍了一遍,对我来说简直就是福音,打算把这些挨个写篇笔记,记录一下。
原型模式
一种用以创建对象的模式,如果要想创建一个对象,一种方法是先指定它的类型,然后通过类来创建一个对象;另一种是,不在关心对象的具体类型,通过克隆创建一个一模一样的对象。
基本规则:
- 所有数据都是对象。
- 得到一个对象不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型。
- 如果对象无法响应某个请求,就会把这个请求 委托 给它的原型。
JavaScript 中的原型继承
JavaScript 同样也遵守上面的基本规则,虽然分为两种类型机制:基本类型( undefined、null、number、boolean、string、symbol
),引用类型( array、object
),除了 undefined
和 null
(它俩没有原型),这些类型都能通过构造函数的形式( 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
属性,找到了并返回它的值。