创建对象的几种方式

这当然不是一篇关于怎么找对象的文章。

在之前阅读时基本都以摘抄要点为主,那样很难有很深的记忆,而且对于阅读的人不是很友好。因此采取动手实践的方式,在阅读的时候整点小demo,看起来直观也更易于理解。

方式总览

大体有 工厂模式 构造函数模式 原型模式 组合使用构造函数模式 动态原型模式 寄生构造函数 稳妥构造函数模式 几种方式,数 组合使用构造函数模式 最为实用。

工厂模式

    const createObject = function () {
        // 创建一个新的对象作为返回值,在该对象上添加属性和方法
        let newObj = {};
        // 将传入的参数赋值到创建的对象中,可以设置任意的参数名和key
        let index = 0;
        while (index < arguments.length) {
            newObj[`arg${index}`] = arguments[index];
            index++;
        }
        // 为创建的对象添加任意方法
        newObj.getProperty = function (property) {
            console.log(this[property]);
        };
        return newObj;
    };

    let obj1 = createObject(11, 22, 33);
    let obj2 = createObject(11, '11', [1, 2, 3]);
    let obj3 = createObject(11, '33', {a: 1});
    obj1.getProperty('arg1');
    obj2.getProperty('arg2');
    obj3.getProperty('arg2');

工厂模式封装了创建对象并为其添加属性和方法的过程。它的优点是减少了创建结构类似的对象时的代码量;不足之处在于只能创建特定数据结构的对象,而且因为工厂内部创建及返回的都是Object,所以它也不能指定通过该模式创建的对象的类型。

构造函数模式

    const ObjectConstructor = function () {
        let index = 0;
        // 和工厂模式不同的是,构造函数将属性和方法赋值在 this 上
        while (index < arguments.length) {
            this[`arg${index}`] = arguments[index];
            index++;
        }
        this.getProperty = function (property) {
            console.log(this[property]);
        };
    };

    let obj1 = new ObjectConstructor(11, 22, 33);
    let obj2 = new ObjectConstructor(11, '11', [1, 2, 3]);
    let obj3 = new ObjectConstructor(11, '33', {a: 1});
    obj1.getProperty('arg1');
    obj2.getProperty('arg2');
    obj3.getProperty('arg2');

构造函数模式在功能上同工厂模式类似,也是封装了为对象添加属性和方法的过程。不同点是:

  • 在工厂模式内部完成创建对象的操作,而在构造函数模式中通过new操作符完成对象的创建(实例化)。
  • 工厂模式中将属性和方法赋到新创建的对象中,而构造函数模式则将其赋到this对象上,此时this对象指向的是使用new操作符创建出来的实例——使用new操作符调用构造函数会经历4个步骤
    创建一个新对象;
    将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
    执行构造函数中的代码(为这个新对象添加属性);
    返回新对象。

通过构造函数模式可以指定创建对象的类型,所以可以使用instanceOf判断其所属类型:

    // 接上文代码
    obj1 instanceof ObjectConstructor  //true

原型模式

    const ObjectConstructor = function () {
    };
    
    /* prototype 属性是一个指针,指向通过构造函数创建的对象实例的原型对象
    *  通过访问原型对象并将属性和方法添加在原型对象中,使得通过构造函数创建的对象实例都能通过原型链访问到这些值
    * */
    ObjectConstructor.prototype.arg1 = 11;
    ObjectConstructor.prototype.arg2 = '33';
    ObjectConstructor.prototype.arg3 = {a: 1};
    ObjectConstructor.prototype.getProperty = function (property) {
        console.log(this[property]);
    };
    
    
    let obj1 = new ObjectConstructor();
    obj1.getProperty('arg1');  // 11
    
    /* 当创建一个新的对象实例,实例内部将包含一个指针 [[Prototype]] 指向构造函数的原型对象
    *  在脚本中没有标准的方式访问 [[Prototype]],但可以尝试以 __proto__ 属性访问
    * */
    console.log(obj1.__proto__);  // 访问原型对象
    // constructor 属性包含一个指向 prototype 属性所在函数的指针
    console.log(obj1.__proto__.constructor === ObjectConstructor);  // true
    
    // 当在实例中声明和原型对象同名的属性时,会屏蔽对原型对象那个属性的访问(在对象实例中就访问到了这个属性)
    obj1.arg1 = 'new property';
    obj1.getProperty('arg1');  // new property
    
    // 通过 delete 操作符删除实例的属性,重建对原型对象上该属性的访问(在对象实例中无法访问到该属性,继续向上到原型对象中访问)
    delete obj1.arg1;
    obj1.getProperty('arg1');  // 11
    
    // 当重写(使用新的Object覆盖)原型对象时,会使 constructor 属性丢失对原型对象的指向,而指向Object构造函数
    ObjectConstructor.prototype = {
        arg1: 'new arg1',
    };
    let obj2 = new ObjectConstructor();
    console.log(obj2.__proto__.constructor);  //指向Object构造函数
    console.log(obj2.__proto__.constructor === ObjectConstructor);  // false
    
    // 因此在重写原型对象时,手动设置其 constructor 指向
    ObjectConstructor.prototype = {
        constructor: ObjectConstructor,
        arg1: 'new arg1',
    };
    let obj3 = new ObjectConstructor();
    console.log(obj3.__proto__.constructor === ObjectConstructor);  // true

不同于前面两种模式在创建对象时是把属性和方法赋给实例对象,原型模式依靠 prototype 属性访问到原型对象并将属性方法挂载在原型对象上。这种方式会带来一个问题:所有基于这个原型对象创建的对象示例,如果没有重写原型对象上的值会使其在访问、修改属性方法时操作到原型对象上的值,从而对其他对象实例产生影响。

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

    const ObjectConstructor = function () {
        let index = 0;
        // 在赋值属性时,使用构造函数模式将其挂载在每个对象实例上,解决原型模式创建的对象共享原型对象上的值的问题
        while (index < arguments.length) {
            this[`arg${index}`] = arguments[index];
            index++;
        }
    };
    // 将方法挂载在原型对象下,使得创建的实例都能共享方法
    ObjectConstructor.prototype.getProperty = function (property) {
        console.log(this[property]);
    };
    let obj1 = new ObjectConstructor(11, '33', {a: 1});
    obj1.getProperty('arg2');

混用构造函数和原型模式的操作将不需要共享的属性挂载在原型对象的实例上避免了不同对象实例对属性的操作影响到其他实例,而将需要共享的方法挂载在原型对象上又很好地复用了方法。

以上三种为主要的创建对象的方式,书本还介绍了另外几种方式。

动态原型模式

直接copy了书本代码,没什么好说?

    function Person(name, age, job) {
        //属性
        this.name = name;
        this.age = age;
        this.job = job;
        // 方法
        if (typeof this.sayName != "function") {
            Person.prototype.sayName = function () {
                alert(this.name);
            };
        }
    }

寄生构造函数模式

使用构造函数的方式创建对象,内部实现和工厂模式一致。也没什么好说?请直接搜索代码实现。

稳妥构造函数模式

实现基本同工厂模式一致,只通过返回的对象中提供的方法去访问参数。也没什么好说?请直接搜索代码实现。