对象的创建与使用

如何创建对象,或者说,如何更优雅的创建对象,一直是一个津津乐道的话题。

以下从单一对象的创建( 普通创建 )入手,逐步介绍 5 种( 批量 )创建对象的模式( 推荐:组合模式)。


普通创建( 单一对象 )

普通创建,即单一对象的创建( 推荐:字面量形式,因为简洁 ),主要包括以下 3 种形式:

(1)new 关键字

对象可以通过执行 new 操作符 + 要创建的构造器对象的名称( 即,构造函数 )来创建,如 new Object()、new Array() ...

Then,创建 Object 类型的实例,并为其添加属性和(或)方法,就可以实现一个自定义对象

[1] 不传参数

        const obj = new Object();  // 创建一个无属性的空对象
        // 说明:如果不给构造函数传递参数,也可以不加括号
        const person = new Object;
        console.log(obj);  // {}
        console.log(person);  // {}

        // 添加属性
        person.name = '小明';
        person.age = 18;
        console.log(person);  // {name: '小明', age: 18}

[2] 传递参数

        // 如果传递的是一个原始类型的值,则返回该值的包装对象
        const obj1 = new Object(123);
        console.log(obj1);  // Number {123}
        console.log(new Object('abc'));  // String {'abc'}
        // undefined 和 nul,会转换为一个空对象
        console.log(new Object(undefined)); // {}
        console.log(new Object(null));  // {}

        // 如果传递的参数是一个对象,则会直接直接返回这个对象
        const obj2 = {a:1};
        const obj3 = new Object(obj2);
        console.log(obj3,obj2 == obj3);  // {a: 1} true
        obj3.a = 100;
        console.log(obj2);  // {a: 100}  // 浅拷贝

        // 不使用 new 操作符,直接用 Object() 函数,相当于转换方法,也可以把任意值转换为对象
        function isObject (value){
            // 判断一个变量是否为对象
            return value === new Object(value);
        }
        console.log(isObject([]));  // true
        console.log(isObject(true));  // false

(2)对象字面量( 常用 )

一般的,创建单一对象的时候,我们通常使用对象字面量的形式,因为它足够简洁;

实际上,对象字面量只是隐藏了和 new 操作符相同的基本过程,所以,可以称之为一种“语法糖”。


对象字面量,是由若干名值对组成的映射表,名值对之间以冒号分隔,整个映射表以 {} 括起来。

一组名值对称为一个属性,属性间以逗号分隔:

  • 属性名:可以是任意字符串
  • 属性值:可以是任意类型表达式,而表达式的值就是属性值
        const obj = {};  // 定义一个空对象,等价于 const obj = new Object();

        const student = {
            'name':'小明',
            'score':58 + 96
        }
        console.log(student);  // {name: '小明', score: 154}

        // 推荐:使用对象字面量的形式定义对象,属性名会自动转换为字符串,所以,可以简写为:
        const stu = {
            name:'小明',
            score:154
        }

        // 低版本兼容,一般地,对象字面量的最后一个属性后的逗号将忽略,但在IE7-浏览器中会报错(SCRIPT1028: 缺少标识符、字符串或数字)
        const stu1 = {
            name:'小明',
            score:154,
        }

(3)基于原型创建 Object.create()

ES5 定义了一个 Object.create() 方法,用于创建一个基于原型的新对象,其包括两个参数:

[1] 第一个参数,就是这个对象的原型对象

        const obj1 = {
            name:'小明',
            age:18
        }
        const obj2 = Object.create(obj1);
        console.log(obj2); // {}
        // obj2 继承了 obj1
        console.log(obj2.name);  // 小明
        // 换句话说就是,obj1 是 obj2 的原型对象
        console.log(obj2.__proto__);   // {name: '小明', age: 18}
        
        obj1.sex = '男';
        console.log(obj2.sex);  // 男

        // 基于null为原型,可以创建一个空对象,但该对象不会继承任何东西,甚至是基础方法toString()和valueOf()
        const obj3 = Object.create(null);
        console.log(obj3);  // {}
        console.log(obj3.toString());  // Uncaught TypeError: obj3.toString is not a function

        // 创建一个普通的空对象(像通过{}或new Object一样),可以传入Object的原型对象,即Object.prototype
        const obj4 = Object.crete(Object.prototype);

[2] 第二个参数,是属性描述符,可选,用于对对象的属性进行进一步的描述

        const obj5 = {
            name:'小明',
            age:18
        }
        const obj6 = Object.create(obj5,{
            x:{value:1,writable: false,enumerable:true,configurable:true},
            y:{value:2,writable: false,enumerable:true,configurable:true}
        })
        console.log(obj6);  // {x: 1, y: 2}
        console.log(obj6.name);  // 小明
        console.log(obj6.x);  // 1

批量创建( 模式 )

如果我们要创建多个对象,通过上述方式,会产生大量的重复代码,该怎么办呢 ?

(1)工厂模式          // 解决了批量创建对象的代码冗余问题,但存在对象类型识别问题

( 1 + 2 )寄生构造函数模式稳妥构造函数模式

(2)构造函数模式  —  构造函数拓展模式            // 解决了对象类型识别问题,但存在( 公共 )方法重复创建的问题

( 2 + 3 )组合模式( 推荐 )

// 组合模式,目前使用最广泛的一种模式,即构造函数模式 + 原型模式,其中:构造函数模式,用于定义实例属性,而,原型模式,用于定义方法和共享属性;该模式还支持像构造函数传递参数。

(3)原型模式  —  更简单的原型模式( 对象字面量写法 )

// 该模式的特点在于“共享”,但存在值属性会被所有实例共享并修改的问题


[1] 工厂模式                                                           // 对象识别问题

解决上述问题,可以使用工厂模式,它抽象了创建具体对象的过程,用函数来封装以特定接口来创建对象的细节。

        function createPerson(name,age){
            const person = new Object();
            person.name = name;
            person.age = age;
            person.say = function(){
                console.log(this.name);
            }
            return person;
        }

        const person1 = createPerson('小明',18);
        const person2 = createPerson('小红',19);
        console.log(person1,person2);  // {name: '小明', age: 18, say: ƒ} {name: '小红', age: 19, say: ƒ}
        console.log(typeof person1);  // Object
        console.log(person1.constructor);  // ƒ Object() { [native code] }

工厂模式解决了批量创建多个相似对象的问题,但,没有解决对象识别的问题,也就是说,没有给出明确的对象类型。


[2] 构造函数模式                                                       // 共享问题

构造函数模式,指的是通过创建自定义的构造函数,来定义自定义对象类型的属性和方法。

        // 该模式没有显式的创建对象,直接将属性和方法赋给了this对象,且没有return语句
        function Person(name,age){
            this.name = name;
            this.age = age;
            this.say = function(){
                console.log(this.name);
            }
        }
        
        const person3 = new Person('小明',18);
        const person4 = new Person('小红',19);
        console.log(person3,person4);  // Person {name: '小明', age: 18, say: ƒ} Person {name: '小红', age: 19, say: ƒ}
        console.log(typeof person3);  // Object
        console.log(person3.constructor)  // ƒ Person(name,age){...}

问题在于,每个方法在每个实例上都会创建一遍,对于创建多个完成相同任务的( 公共 )方法时,没必要浪费内存。

        // 具有相同作用的say方法在两个实例中占用了不同的内存空间
        console.log(person3.say === person4.say);  // false

[2-1] 构造函数( 拓展 )模式

        // 在构造函数模式的基础上,将方法的定义转移到构造函数外部,可以解决方法被重复创建的问题
        function Person(name, age) {
            this.name = name;
            this.age = age;
            this.say = say;
        }
        function say() {
            console.log(this.name);
        }
        const person3 = new Person('小明',18);
        const person4 = new Person('小红',19);
        console.log(person3.say === person4.say);  // true

问题在于,在全局作用域中定义函数,实际上只能被某个对象调用,这让全局作用域有点名不副实;而且,如果对象需要定义多个方法,就需要定义很多全局函数,会严重污染全局空间,这个自定义引用类型就没有封装性可言了。


[2-2] 寄生构造函数模式( 应尽量避免使用 )

该模式是工厂模式和构造函数模式的结合,其基本思想是:创建一个函数,用于封装对象的代码,并返回新创建的对象。

            function Person(name,age){
                const person = new Object();
                person.name = name;
                person.age = age;
                person.say = function(){
                    console.log(this.name);
                }
                return person;
            }

            const person1 = new Person('小明',18);
            const person2 = new Person('小红',19);

该模式与构造函数模式有着相同的问题 — 每个方法都要在每个实例上被创建一遍,对于公共方法完全没必要,浪费内存。

            // 具有相同作用的say方法在两个实例中占用了不同的内存空间
            console.log(person1.say === person2.say);  // false

同时,该模式返回的对象和构造函数之间也没有关系,也就是 instanceof 和 prototype,都没有意义。

            // 返回的对象与构造函数之间没有关系,instanceof 运算符和 prototype 属性都没有意义
            console.log(person1 instanceof Person);  // false
            console.log(person2.__proto__  === Person.prototype);  // false

[3] 稳妥构造函数

所谓稳妥对象,指的是没有公共属性,其方法也不会引用 this 的对象。

稳妥对象,比较适合在一些安全环境( 这些环境会禁用 this 和 new )或者防止数据被其他应用程序改动时使用。

该模式与寄生构造函数模式类型,但有两点不同:

新创建对象的实例方法不引用 this

不使用 new 操作符调用构造函数

            function Person(name,age){
                const person = new Object();
                // ... 这里可以定义私有属性和方法
                person.say = function(){
                    console.log(name);
                }
                return person;
            }

            // 在稳妥对象中,除了使用say方法外,没有其他方法访问name的值
            const person1 = Person('小明',18);
            person1.say();  // 小明

其问题与寄生构造函数类似,所创建的对象与构造函数之间也没什么关系,所以,instanceof 对这种对象没什么意义。


[3] 原型模式

原型对象,可以让其所有实例共享它的属性和方法。

也就是说,不需要在构造函数中定义实例的属性和方法,可以直接将这些属性和方法添加到原型对象中。

                function Person(){
                    // 说明:构造函数的prototype属性指向原型对象
                    Person.prototype.name = '小明';
                    Person.prototype.age = 18;
                    Person.prototype.say = function(){
                        console.log(this.name);
                    }
                }

                const person1 = new Person();
                console.log(person1.age);  // 18
                person1.say();  // 小明
                const person2 = new Person();
                console.log(person2.name);  // 小明
                console.log(person1.say === person2.say);  // true

[3-1] 原型对象模式的简化写法

为了减少不必要的输入,从视觉上更好的封装原型的功能,可以用一个对象字面量来重写原型对象。

        function Person() {}
        Person.prototype = {
            name: '小明',
            age: 18,
            say: function () {
                console.log(this.name);
            }
        }
        const person1 = new Person();
        console.log(person1.name);  // 小明

但是,因为该模式重写了默认的 prototype 对象,使得 Person.prototype 的自有属性 constructor 属性不存在;

只有从原型链中找到 Object.prototype 中的 constructor 属性。

        console.log(person1.constructor === Person);  // false
        console.log(person1.constructor === Object);  // true

==> 可以显式的设置原型对象的 constructor 属性:

        function Person() {}
        Person.prototype = {
            constructor:Person,
            name: '小明',
            age: 18,
            say: function () {
                console.log(this.name);
            }
        }
        const person1 = new Person();
        console.log(person1.name);  // 小明

        console.log(person1.constructor === Person);  // true
        console.log(person1.constructor === Object);  // false

默认情况下,原生的 constructor 属性不可枚举,

==> 更妥善的方法是使用 Object.defineProperty() 方法,改善其属性描述符中的枚举性 enumerable。

        function Person() {}
        Person.prototype = {
            name: '小明',
            age: 18,
            say: function () {
                console.log(this.name);
            }
        }
        Object.defineProperty(Person.prototype,'constructor',{
            enumerable:false,
            value:Person
        })
        const person1 = new Person();
        console.log(person1.name);  // 小明

        console.log(person1.constructor === Person);  // true
        console.log(person1.constructor === Object);  // false

原型模式,其问题在于,引用类型值属性会被所有的实例对象共享并修改,这也是很少有人单独使用原型模式的原因。

function Person(){}
Person.prototype = {
    constructor: Person,
    name: "zhangsan",
    age: 18,
    friend : ["shelby","Court"],
    sayName: function(){
        console.log(this.name);
    }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends);   // ["shelby","Court","Van"];
alert(person2.friends);   // ["shelby","Court","Van"];
alert(person1.friends === person2.friends);   // true

组合模式(推荐)

组合使用构造函数模式和原型模式,是创建自定义类型的最常见方式。

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这种模式还支持向构造函数传递参数。

  • 实例对象都有自己的一份实例属性的副本,同时又共享对方法的引用,最大限度地节省了内存。
  • 该模式是目前使用最广泛、认同度最高的一种创建自定义对象的模式。
function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends = ["shelby","Court"];
}
Person.prototype = {
    constructor: Person,
    sayName : function(){
        console.log(this.name);
    }    
}
var person1 = new Person("zhangsan",18);
var person2 = new Person("lisi",20);
person1.friends.push("Van");
alert(person1.friends);   // ["shelby","Court","Van"];
alert(person2.friends);   // ["shelby","Court"];
alert(person1.friends === person2.friends);   // false
alert(person1.sayName === person2.sayName);   // true

[ 动态原型模式 ]

动态原型模式,将组合模式中分开使用的构造函数和原型对象都封装到了构造函数中,然后通过检查方法是否被创建,来决定是否初始化原型对象。使用这种方法将分开的构造函数和原型对象合并到了一起,使得代码更加整齐,也减少了全局空间的污染;如果原型对象中包含多个语句,只需要检测其中一个语句即可。

function Person(name,age){
    // 属性
    this.name = name;
    this.age = age;
    // 方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        };
    }
}
var friend = new Person("zhangsan",18);
friend.sayName();   // 'zhangsan'