JavaScript基础 - 如何创建对象?

208 阅读6分钟

面向对象的程序设计,核心在于“对象”。在ECMAScript里面,可以把对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。 在加下来的篇幅里面,跟大家一起聊一聊如何创建对象,以及他们的优缺点,也会涉及到一些应用场景。

工厂模式

考虑到在ES6之前无法创建类,发明了一种函数,用函数来封装以特定接口创建对象的细节。

function createClerk(name, turnover, avatar) {
    const o = new Object();
    o.name = name;
    o.turnover = turnover;
    o.avatar = avatar;
    o.moveFruits = function(fruits, onePlace, anotherPlace) {
        // move the fruits from one place to another place
    }
    return o;
}
const a = createClerk('peter', 20, 'https://xxx');
const b = createClerk('tom', 30, 'https://yyy');

通过在函数createClerk()能够根据接收的参数来构建一个包含所有必要信息的Clerk对象。解决了多个相似对象的问题,但是没有解决对象识别的问题(即怎样知道一个对象的类型),对应上面的例子,即为:

// 在chrome打开Console调试
> a.constructor
// f Object() { [native code] }
> b.constructor
// f Object() { [native code] }
> const c = [1, 2]
> c.constructor
// f Array() { [native code] }

可以看出c是一个数组类型的对象,但是基于createClerk()创建的ab无法被识别对象类型。

构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象。像ObjectArray这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如:

function Clerk(name, turnover, avatar) {
    this.name = name;
    this.turnover = turnover;
    this.avatar = avatar;
    this.moveFruits = function (fruits, onePlace, anotherPlace) {
        // move the fruits from one place to another place
    }
}
const a = new Clerk('peter', 20, 'https://xxx');
const b = new Clerk('tom', 30, 'https://yyy');

通过与 工厂模式 创建对象比较,有以下几点的不同:

  • 没有显式的创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return语句

同时,要创建Clerk的新实例,必须使用new操作符。这种方式调用构造函数,会经理4个步骤:

创建一个新对象 将构造函数的作用域赋给新对象(因此this就指向了这个新对象) 执行构造函数中的代码 返回新对象

> a.constructor
// f Clerk() { [native code] }
> a instanceof Clerk
// true
> a instanceof Object
// true

构造函数虽然好用,但也并非没有缺点。主要问题就是,每个方法都要在每个实例上面重新创建一遍。

> a.moveFruits == b.moveFruits
// false

很容易想到,把moveFruits放在全局即可解决这个问题。

function Clerk(name, turnover, avatar) {
    this.name = name;
    this.turnover = turnover;
    this.avatar = avatar;
    this.moveFruits = moveFruits;
}
function moveFruits = function (fruits, onePlace, anotherPlace) {
    // move the fruits from one place to another place
}
const a = new Clerk('peter', 20, 'https://xxx');
const b = new Clerk('tom', 30, 'https://yyy');

但是又带来新的问题:

  • 在全局作用域中定义的函数实际上只能被某个对象调用,有点名不副实;
  • 对象定义需要很多方法,就要创建多个全局函数,我们自定义的引用类型就丝毫没有封装性可言。

原型模式

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象可以被通过函数创建的实例对象共享属性和方法。

function Clerk(){}
CLerk.prototype.name = 'peter';
Clerk.prototype.turnover = 20;
Clerk.prototype.avatar = 'https://xxx';
Clerk.prototype.moveFruits = function (fruits, onePlace, anotherPlace) {
    // move the fruits from one place to another place
}

也可以简写为:

function Clerk() {}
Clerk.prototype = {
    constructor: Clerk,
    name: 'peter',
    turnover: 20,
    avatar: 'https://xxx',
    moveFruits: function (fruits, onePlace, anotherPlace) {
        // move the fruits from one place to another place
    }
}

这样的模式解决了构造函数模式的函数实例重复创建的问题,但是也带来了一些新的问题:

  • 省略了构造函数初始化传参这一环节,结果所有实例默认情况下都将取得相同的属性值;
  • 最大的问题:实例共享属性
function Clerk() {}
Clerk.prototype = {
    constructor: Clerk,
    name: 'peter',
    turnover: 20,
    avatar: 'https://xxx',
    interests: ['swim', 'mountaineering']
    moveFruits: function (fruits, onePlace, anotherPlace) {
        // move the fruits from one place to another place
    }
}
const a = new Clerk();
const b = new Clerk();

a.interests.push('electron');
console.log(b.interests);
// ['swim', 'mountaineering', 'electron']

组合使用构造函数模式和原型模式

创建自定义对象最常见的方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省内存。也支持构造函数传参。

function Clerk(name, turnover, avatar) {
    this.name = name;
    this.turnover = turnover;
    this.avatar = avatar;
    this.interests = ['swim', 'mountaineering'];
}

Clerk.prototype = {
    constructor: Clerk,
    moveFruits: function (fruits, onePlace, anotherPlace) {
        // move the fruits from one place to another place
    }
}
const a = new Clerk('peter', 20, 'https://xxx');
const b = new Clerk('tom', 30, 'https://yyy');

a.interests.push('electron');
console.log(b.interests)
// ['swim', 'mountaineering']
console.log(a.moveFruits === b.moveFruits)
// false

动态原型模式

熟悉其它语言OO编程的人,看到这种模式,可能会比较困惑。动态原型就是为了解决这样的问题。把所有的信息封装在构造函数中,通过构造函数初始化原型。原理就是,通过检查某个方法是否应该有效,来决定是否需要初始化原型

function Clerk(name, turnover, avatar) {
    this.name = name;
    this.turnover = turnover;
    this.avatar = avatar;
    this.interests = ['swim', 'mountaineering'];
    if (typeof Clerk.prototype.moveFruits !== 'function') {
        Clerk.prototype.moveFruits = function (fruits, onePlace, anotherPlace) {
            // move the fruits from one place to another place
        }
    }
}

需要注意的是:使用动态原型模式时,不能使用对象字面量重写原型。Clerk.prototype = { ... },这样在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系。

寄生构造函数模式

这种模式的基本思维是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

function Clerk(name, turnover, avatar) {
    const o = new Object();
    o.name = name;
    o.turnover = turnover;
    o.avatar = avatar;
    o.moveFruits = function (fruits, onePlace, anotherPlace) {
        // move the fruits from one place to another place
        console.log(this)
    }
    return o;
}
const a = new Clerk('peter', 20, 'https://xxx');

除了使用new操作符,跟工厂模式其实是一模一样的。构造函数在默认不返回值的情况下,默认返回新对象实例。

这种模式在特殊情况下用来为对象创建构造函数。例如:我们要创建一个具有特殊方法的Array。但是不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray() {
    const values = new Array();
    values.push.apply(values, arguments);
    values.toPipedString = function() {
        return this.join('|');
    };
    return values;
}
const colors = new SpecialArray(['red', 'blue', 'green']);
console.log(colors.toPipedString());
// 'red|blue|green'

注意:通过寄生构造函数模式创建的实例和函数的原型没有关系,不能依赖instanceof来确定对象类型。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。与寄生构造函数模式类似,但是有两点区别:

  • 不使用new来调用构造函数
  • 新创建对象的实例不引用this
function Clerk(name, turnover, avatar) {
    const o = new Object();
    
    o.getName = function() {
        return name;
    }
    
    o.getAvatar = function() {
        return avatar;
    }
    
    o.getTurnover = function() {
        return turnover;
    }
    
    return o;
}

可以看出,除了调用getName()方法之外,没有别的方式可以访问其数据成员。稳妥构造函数模式提供这种安全性,使得它非常适合在某些安全执行环境。

同时,与寄生构造函数模式类似,instanceof对这种对象也没有意义。