JavaScript 的面向对象

200 阅读5分钟

一 两种面向对象模型

1. 基于类的面向对象

最为大众熟知的应该是经典的基于类的面向对象模型。在这个模型中,开发者通过 “分类” 或 “归类” 的手法将事物抽象为一个个的 “类”。类定义了事物的特征(属性),以及改变特征的操作(方法)。对象是类的实例化产物,程序中真实存在的是一个个的对象。

2. 基于原型的面向对象

JavaScript 中采用的是基于原型的面向对象。这种模型没有 “类” 的概念。原型更接近于人类的描述方式,它不对事物做 “分类” 或 “归类”,而是采用 “相似” 的方式去描述对象,即描述一个未知对象与一个已知对象的区别。

二 JavaScript 对象模型

1. 对象的属性与行为

任何一个对象都是唯一的,这与它自身的状态无关。即便两个所有状态都一致(属性都相等)的对象,那也是不同的对象。使用状态来描述对象,使用行为来改变状态。因此,任何没有改变状态的函数不应该被封装为对象的方法

2. 对象模型

JavaScript 的对象除了属性和方法,还有原型存在。因为属性是用于描述自身状态,但原型严格来说并不是 “自身” 的,因此,原型不应该被视为属性。

JavaScript 对象模型

3. 属性与属性值

JavaScript 的对象属性只能是两种数据类型:

    1. Symbol
    1. String (Number 会被转换成 String)

JavaScript 的对象属性值可能是两种类型:

    1. 数据型的属性(Data)
    1. 访问器型的属性(Accessor)

(1) 数据型属性

数据型属性

数据型属性有四个特性(Attribute),分别是:

  • [[value]]:属性的值
  • writable:属性值是否可写,如果为 false,属性值被初始化后就不可更改(至少需要初始化时写一次)
  • enumerable:是否可被枚举,如果为 falsefor...in 将不会枚举到该属性
  • configurable:这三个特性(writableenumerableconfigurable)能否被改变。如果为 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