阅读 202

JavaScript中创建对象的几种方式

前言

一般情况下,JS中创建对象的方式可以用构造函数Object或者对象字面量的方式,但需要创建几个具有相同属性或方法的对象时,就得写大量的冗余代码。故而出现了下述几种创建对象的方法。

一、工厂模式

工厂模式是一种常见的设计模式。这种模式把对象的创建过程抽象出来并封装成一个函数。需要使用同类型的对象时,可以不断地调用此函数。例子如下:

function createPerson (name, age) {
    var o = new Object();
    o.name = name;
    o.sayName = function(){
        console.log(this.name)
    }
    return o;
}
复制代码

工厂模式解决了创建多个相同类型对象的问题,即减少了代码的冗余,但没有解决对象识别的问题。

二、构造函数模式

JavaScript中的构造函数其实就是普通的函数,但构造函数的目的一般用来创建对象。按照约定俗成的惯例,构造函数应该以一个大写字母开头。例子如下:

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name);
    }
}
var xiaoming = new Person('小明', 18);
var xiaohong = new Person('小红', 16);
xiaoming.sayName(); //小明
xiaohong.sayName(); //小红
复制代码

此例子与工厂模式的区别有:

  • 没有显式地创建一个对象;
  • 直接把方法和属性值赋给this;
  • 没有return语句;
  • 创建对象时需要使用new关键字。

此外,构造函数模式还解决了对象识别的问题:

xiaoming instanceof Person // true
xiaohong instanceof Person // true
复制代码

特别地,因为构造函数也是函数,在不用new操作符调用时,函数内部的this对象其实是指向了全局对象(浏览器环境是window对象),具体原因请学习this的原理。

构造函数并不是没有缺点,如上面例子的sayName方法,在每个实例上都重新创建了一遍:

xiaoming.sayName === xiaohong.sayBane // false
复制代码

像sayName这种函数都是为了完成相同的任务(打印实例的名字),大可抽象出来作为一个共享的函数

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}
var xiaoming = new Person('小明', 18);
var xiaohong = new Person('小红', 16);
xiaoming.sayName();  //小明
xiaohong.sayName();  //小红
复制代码

但这种方式使得sayName函数变为了全局函数,并且只合适被Person new出来的实例调用。故而带来下面几个问题:

  • 此全局函数会让其他的开发人员感到疑惑;
  • 使得全局作用域变得名副其实(全局作用域的属性和函数应该适合在任何地方调用);
  • 若构造函数的实例需要很多方法时,会暴露太多函数到全局作用域,毫无封装性可言。

三、原型模式

利用JS的原型对象和原型链,可以解决构造函数模式中带来的那几个问题:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: '小明',
    age: 18,
    sayName: function () {
        console.log(this.name)
    }
}

var xiaoming = new Person();
var xiaohong = new Person();
xiaoming.sayName(); //小明
xiaohong.sayName(); //小明
复制代码

上面的例子可以看到,虽然解决了构造函数模式存在的问题,但缺点也是大大的:

  • 因忽略了构造函数的传参,每个实例的属性都是相同的,缺乏灵活性;
  • 对于引用类型的属性值,一个实例对其修改会反映到所有实例。
Person.prototype.friends = ['张三', '李四'];
xiaoming.friends === xiaohong.friends; //true
xiaoming.frinds.push('王五');
xiaohong.friends; // ['张三', '李四', '王五'];
复制代码

四、组合使用构造函数模式和原型模式(推荐)

组合使用构造函数模式和原型模式是ES5中创建对象最常用的模式,此方式用构造函数定义实例的公共属性,使用原型模式定义实例的公共方法,最大限度降低了内存:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.friends = ['张三', '李四'];
};
Person.prototype.sayName = function () {
    console.log(this.name);
}
var xiaoming = new Person('小明', 18);
var xiaohong = new Person('小红', 16);
xiaoming.sayName(); //小明
xiaohong.sayName(); //小红
复制代码

另外,对于引用类型的共享的属性值,实例之间不会互相影响:

xiaoming.friends ==== xiaohong.friends; //false
xiaoming.frinds.push('王五');
xiaohong.friends; // ['张三', '李四'];
复制代码

五、动态原型模式

动态原型模式就是把所有信息封装在构造函数里:

function Person(name, age){
    this.name = name;
    this.age = age;
    if(typeof this.sayName !== 'function'){
        Person.prototype.sayName = function () {
            console.log(this.name);
        }
    }
};
var xiaoming = new Person('小明', 18);
var xiaohong = new Person('小红', 16);
xiaoming.sayName(); //小明
xiaohong.sayName(); //小红
复制代码

动态原型模式同事保持了构造函数模式和原型模式的优点,且封装性比构造函数和原型组合使用的方式更好。但要注意在构造函数执行后不能重写原型对象,否则会切断现有实例和新原型的关系。因为该模式中原型对象上的自定义方法是在构造函数中挂载的。

红皮书还介绍了寄生构造函数模式和稳妥构造函数模式,但实践中非常少用。有兴趣的同学可以查阅。