在 JavaScript 中面向对象编程

「面向对象编程」即「OOP」(下文都用此称呼)是使用 JS 时最直觉最基本的编程范式。

类式继承

由于很多人在进行 OOP 时更熟悉像 Java 这种基于类的编程方式,因而在用 JS 编程时会去追逐类的影子,进而产生了一种模拟类的继承方式——类式继承(classical inheritance)。

这部分内容的语境置于基于类的编程方式中,所以「对象」是类,或者说构造函数,实例化的产物,而非 JS 中对象本身的意义。

传统做法

在尚未有语法层面的支持之时,人们利用构造函数与原型链去模拟类。

假设父构造函数及其原型定义如下:

function Parent(name) {
this.name = name || 'Adam';
}

Parent.prototype.say = function() {
return this.name;
}

下面为各种不同的子构造函数定义及继承方式——

默认模式

function Child(name) {}

Child.prototype = new Parent();

缺点:子构造函数的参数无法传入父构造函数中。

借用构造函数

Rent-a-Constructor

function Child(name) {
Parent.apply(this, arguments);
}

缺点:父构造函数原型上的方法没被继承。

借用和设置原型

function Child(name) {
Parent.apply(this, arguments);
}

Child.prototype = new Parent();

共享原型

function Child() {}

Child.prototype = Parent.prototype;

优点:缩短子对象访问方法时在原型链上的查找路径。

缺点:修改子构造函数的原型会影响到父构造函数的原型。

临时构造函数

// 利用 IIFE 创建闭包以避免代理构造函数被反复创建
var inherit = (function() {
var F = function() {};

return function(C, P) {
// 使用代理以避免子孙对象的修改影响到祖先
F.prototype = P.prototype;
C.prototype = new F();

// 模拟「超类」
C.super = P.prototype;

// 纠正指向以在类型检查时得到「正确」的结果
C.prototype.constructor = C;
}
})();

function Child(name) {
Parent.apply(this, arguments);
}

inherit(Child, Parent);

现代做法

有了语法层面的支持,若用基于类的方式编程,体验变得更好了!

ES Classes

从 ES6 开始在语法层面支持了以类的方式进行定义,即便底层依然是基于原型实现的。

class Parent {
#name;

constructor(name) {
this.name = name || 'Adam';
}

say() {
return this.name;
}
}

class Child extends Parent {}

TypeScript Classes

在 TS 中天然支持类,并且更为「标准」。

class Parent {
private name: string = '';

constructor(name: string = 'Adam') {
this.name = name;
}

public say(): string {
return this.name
}
}

class Child extends Parent {}

原型继承

原型继承(prototypal inheritance)

var parent = {
name: 'Adam',
getName: function() {
return this.name;
},
};

var child = Object.create(parent);

对象复制

对象复制(object copying)技术是通过对已有对象进行「复制」以达到复用的目的,常见方式有浅复制(shallow copy)与深复制(deep copy)两种。

在复制属性时,只复制源对象的自身属性,也就是说,位于原型链上的属性不去复制。

浅复制

浅复制只是简单地将源对象的第一层属性的值赋给目标对象的相应属性上,如果值是对象(数组、函数都算),则在目标对象与源对象中都指向同一个地址。

这就意味着,修改其中一个对象的作为属性值的对象的某个属性,则会影响另一个对象。

内置支持

JS 中在语法或内置 API 层面也有一些进行浅复制的方式:

  • 展开语法 ...
  • Array.prototype.slice()
  • Array.prototype.concat()
  • Array.prototype.map()
  • Array.prototype.filter()
  • Array.from()
  • Object.assign()
  • Object.create()

手动实现

function extend(source, target) {
if (typeof source !== 'object') {
return null;
}

var k;

target = target || (Object.prototype.toString.call(source) === '[object Array]' ? [] : {});

for (k in source) {
if (source.hasOwnProperty(k)) {
target[k] = source[k];
}
}

return target;
}

深复制

深复制通过完全地深度复制解决了浅复制的对象类属性值的修改问题,目标对象中每一层级都是全新的,不与源对象中的属性值共享地址;但也有像函数等例外。

内置支持

若被复制的是个纯对象或由纯对象构成的数组,且不存在循环引用,那就可被序列化,即可使用内置 API:

  • JSON.parse(JSON.stringify())
  • structuredClone()

手动实现

function extend(source, target) {
if (typeof source !== 'object') {
return null;
}

var toStr = Object.prototype.toString;
var arrType = '[object Array]';
var k, v;

target = target || (toString.call(source) === arrType ? [] : {});

for (k in source) {
if (!source.hasOwnProperty(k)) {
continue;
}

v = source[k];

if (typeof v === 'object') {
target[k] = extend(v, toStr.call(v) === arrType ? [] : {});
} else {
target[k] = v;
}
}

return target;
}

该实现中未处理循环引用和特殊类型对象的问题,实际使用时可进一步优化。

混入

在 JS 中,混入(mixin)是一个可被复用的对象,它提供用于默认数据的变量及包含处理数据逻辑的函数。

可利用一个或多个此类对象组合创建出新的对象,或在已有对象上进行功能扩展——达到不去继承就能复用的效果,且可起到类似多重继承的作用。

在实际使用时会用到上面提到的对象复制技术。

将手动实现的 extend() 函数做些调整,让它支持动态参数,并能被参数控制是浅复制还是深复制:

function extend() {
var args = [].slice.call(arguments);
var length = args.length;
var target = args[0] || {};
var deep = false;
var i = 1;
var isArr = function(t) { return Object.prototype.toString.call(t) === '[object Array]'; };
var src, obj, k, v;

if (typeof target === 'boolean') {
deep = target;
target = args[1] || {};
i = 2;
}

while (i < length) {
src = args[i];

if (typeof src === 'object') {
for (k in src) {
if (!src.hasOwnProperty(k)) {
continue;
}

v = src[k];

if (deep && typeof v === 'object') {
if (isArr(v)) {
obj = isArr(target[k]) ? target[k] : [];
} else {
obj = target[k] || {};
}

target[k] = extend(deep, obj, v);
} else {
target[k] = v;
}
}
}

i++;
}

return target;
}

参考