一 两种面向对象模型
1. 基于类的面向对象
最为大众熟知的应该是经典的基于类的面向对象模型。在这个模型中,开发者通过 “分类” 或 “归类” 的手法将事物抽象为一个个的 “类”。类定义了事物的特征(属性),以及改变特征的操作(方法)。对象是类的实例化产物,程序中真实存在的是一个个的对象。
2. 基于原型的面向对象
JavaScript 中采用的是基于原型的面向对象。这种模型没有 “类” 的概念。原型更接近于人类的描述方式,它不对事物做 “分类” 或 “归类”,而是采用 “相似” 的方式去描述对象,即描述一个未知对象与一个已知对象的区别。
二 JavaScript 对象模型
1. 对象的属性与行为
任何一个对象都是唯一的,这与它自身的状态无关。即便两个所有状态都一致(属性都相等)的对象,那也是不同的对象。使用状态来描述对象,使用行为来改变状态。因此,任何没有改变状态的函数不应该被封装为对象的方法。
2. 对象模型
JavaScript 的对象除了属性和方法,还有原型存在。因为属性是用于描述自身状态,但原型严格来说并不是 “自身” 的,因此,原型不应该被视为属性。
3. 属性与属性值
JavaScript 的对象属性只能是两种数据类型:
-
- Symbol
-
- String (Number 会被转换成 String)
JavaScript 的对象属性值可能是两种类型:
-
- 数据型的属性(Data)
-
- 访问器型的属性(Accessor)
(1) 数据型属性
数据型属性有四个特性(Attribute),分别是:
[[value]]:属性的值writable:属性值是否可写,如果为false,属性值被初始化后就不可更改(至少需要初始化时写一次)enumerable:是否可被枚举,如果为false,for...in将不会枚举到该属性configurable:这三个特性(writable,enumerable,configurable)能否被改变。如果为false,那么连configurable也不能改,只有一次改变成false的机会。
(2) 访问器型的属性
访问器型属性有一对 get/set 方法用于读写该数据,因此不需要 writable 属性控制是否可读写。
get/set 本身只是函数,不遵循 get 一定是读取且不改变状态,set 一定是更新这样的规则。
访问器属性(Accessor Property)会在属性访问时执行函数,如果函数内逻辑过多,会造成比较差的性能,因此访问器属性需要慎重使用
三 JavaScript 中的继承
1. 基于原型链的继承
JavaScript 是基于原型的面向对象模型,继承使用的便是基于原型链的继承方式。
在 JavaScript 中存在一个最终的基本对象 —— Object.prototype 指向的对象。所有的对象均是派生自 Object.prototype 指向的对象。这里需要明确一点,Object 是构造器,即构造函数,它的原型对象 Object.prototype 才是最终的对象。
JavaScript 中访问任何一个对象都存在一个原型对象,可以看作该对象派生自该对象的原型对象。JavaScript 访问一个对象的属性时,会先访问该对象本身的属性,如果不存在,会访问该对象的原型对象上的属性,如果也不存在,会访问原型上的原型,以此类推,最后一个被访问的对象便是 Object.prototype 对象,该对象的原型指向 null,因此,如果该对象上也不存在,那么就说明该属性不存在,返回 undefined。这样一条访问链路,体现了 JavaScript 中的继承方式,即原型链。
2. 非函数对象的原型链
许多资料(包含MDN)在解释 JavaScript 如何继承的时候会将对象与函数混在一起解释,这样会比较混乱,这里分开来讨论。
普通对象上存在一个私有属性 __proto__,这个属性指向了该对象的原型对象。
const obj = { a: 'AAA' }
console.log(obj) // {a: "AAA"}
console.log(obj.__proto__ === Object.prototype) // true
console.log(obj.__proto__.__proto__) // null
可以看出,一个由字面量生成的普通对象(没有过手动继承的),它的原型对象就是 Object.prototype,而 Object.prototype 的原型指向了 null。__proto__ 属性是可改的。修改该属性相当于修改了该对象的原型链。
const obj = { a: 'AAA' }
obj.__proto__ = { b: 'BBB' }
console.log(obj) // {a: "AAA"}
console.log(obj.__proto__) // {b: "BBB"}
console.log(obj.__proto__.__proto__ === Object.prototype) // true
console.log(obj.a, obj.b, obj.c) // AAA BBB undefined
由此有了下图来表示 JavaScript 中普通对象(非函数)的继承关系:
3. 函数的原型链
(1) 函数原型
首先,函数在声明或创建时,除了声明了函数本身以外,还会创建一个函数的原型对象,这个原型对象不在常规的继承体系里面,暂且称之为 “函数原型”。这个函数原型只能通过函数的 prototype 属性访问。所有允许被继承的属性,方法,都存放在这个函数原型上。函数原型是一个普通的对象。
const fn = function () {}
console.log(fn.prototype) // {constructor: ƒ}
console.log(fn.prototype.__proto__ === Object.prototype) // true
普通对象是没有 prototype 属性的。
console.log(({}).prototype) // undefined
(2) 函数的原型对象
需要明确的是,JavaScript 中,函数也是对象。因此,函数也存在 __proto__ 属性,指向该函数的原型对象。在不修改函数原型的情况下,每一个函数(无论是函数声明创建的还是函数表达式创建的)都是派生自 Function.prototype。
const fn = function () {}
console.log(fn.__proto__) // ƒ () { [native code] }
console.log(fn.__proto__ === Function.prototype) // true
console.log(fn.__proto__.__proto__ === Object.prototype) // true
JavaScript 中所有对象最终都派生自 Object.prototype,标准内置对象也不例外。Function.prototype 便是派生自 Object.prototype。因此会有下面的结果:
console.log(fn.__proto__.__proto__ === Object.prototype)
// true
console.log(fn.__proto__.__proto__ === fn.prototype.__proto__)
// true
因此,不严谨的说,函数的原型链会存在一个 prototype 分支,但最终都是继承自 Object.prototype。
(3) 箭头函数
箭头函数依然派生自 Function.prototype,不同的是它没有函数原型。
const arrow = () => {}
console.log(arrow.prototype) // undefined
console.log(arrow.__proto__ === Function.prototype) // false