说说 JavaScript 创建对象的各种模式

之前说的使用 Object 构造函数或者对象字面量来创建对象,都有一个缺点:使用同一个接口创建多个对象,会产生大量重复的代码。为了解决这个问题,人们考虑了很多种对象创建模式。

1 工厂模式

这种模式抽象了创建具体对象的过程,它使用函数来封装特定接口创建对象的细节:

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

var person1=createPerson("deniro",29,"Software Engineer");
var person2=createPerson("Lily",15,"Doctor");
console.log(person1);
console.log(person2);

工厂模式的问题是无法识别一个对象的类型。

2 构造函数模式

创建一个自定义的构造函数,里面包含自定义对象类型的属性和方法:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    };
}

var person1 = new Person("deniro", 29, "Software Engineer");
var person2 = new Person("lily", 15, "Doctor");

这里的 Person() 函数与之前的 createPerson() 函数有这些不同:

  • 没有显示创建对象。
  • 直接把属性和方法赋值给 this。
  • 没有 return。

注意: 自定义构造函数的命名惯例是首字母大写,比如这里的 Person,非构造函数的命名是首字母小写。还有构造函数也是函数,只不过可以用来创建对象罢了。


使用 new 操作符创建 Person 实例,它会经历以下 4 步:

  1. 创建一个新对象。
  2. 将构造函数的作用域赋给新对象。(this 就指向新对象)
  3. 执行构造函数中的代码。
  4. 返回新对象。

person1 和 person2 分别保存着 Person 的一个不同的实例,它们都有一个 constructor 属性(构造函数),都指向 Person:

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

相对来说,使用 instanceof 检测对象类型比 constructor 更可靠些,还要注意一点,我们所创建的所有对象都是 Object 的实例:

console.log(person1 instanceof Object);//true
console.log(person2 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Person);//true

2.1 把构造函数当作函数

构造函数也是函数,它与其他函数的唯一区别就是调用方式不同。只要通过 new 操作符来调用某个函数,这个函数就可以作为构造函数。前面创建的 Person() 函数可以通过以下任何一种方式被调用:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    };
}

//当作构造函数被调用
var person = new Person("deniro", 19, "Software Engineer");
person.sayName();//deniro

//当作普通函数被调用
Person("lily", 15, "dreamer");//添加到 window
window.sayName();//lily

//在另一个对象的作用域中被调用
var o = new Object();
Person.call(o, "jack", 19, "thinker");
o.sayName();//jack

2.2 构造函数的问题

构造函数的问题是定义的每个方法,都要在每个实例上重新创建一遍。因为函数其实是对象,因此每定义一个函数,也就实例化了一个对象:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function ("console.log(this.name)");//这与之前的的函数声明是等价的
}

因此,不同实例上的同名函数是不等的:

console.log(person1.sayName == person2.sayName);//false

创建两个完成相同功能的 Function 实例完全没有必要,因此可以吧函数定义转移到构造函数外部来解决这个问题:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName() {
    console.log(this.name);
}

var person1 = new Person("deniro", 29, "Software Engineer");
var person2 = new Person("lily", 15, "dreamer");

但这样做的新问题是,在全局作用域中定义的函数要求只能被某个对象调用,这与全局作用域的概念是相悖的,而且如果对象需要定义很多方法,那么就要定义很多个全局函数,这样我们自定义的引用类型就几乎没有任何封装性可言了。

3 原型模式

每个函数都有一个 prototype 属性,它是一个指针,指向一个对象,这个对象包含可以由特定类型的所有实例所共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法:

function Person() {
}

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var person1 = new Person();
person1.sayName();//deniro

var person2 = new Person();
person2.sayName();//deniro

console.log(person1.sayName == person2.sayName);//true

3.1 理解原型对象

只要创建了新函数,就会为这个函数创建一个 prototype 属性,这个属性指向函数的原型对象。ECMAScript 5 把这个属性指针叫做[[Prototype]],没有标准方式可以访问[[Prototype]],但在 Firefox、Safari、Chrome 中把它叫做 __proto__。这时原型对象会自动获得一个 constructor 属性,他包含一个指向 prototype 属性所在函数的指针。

创建自定义函数后,原型对象默认只会取得 constructor 属性,其他方法都是从 Object 继承来的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6vqpau3J-1588381601433)(http://p1.bqimg.com/567571/b69f29cbbf06fad2.png)]

使用 isPrototypeOf() 方法来确定某个对象是否是另一个对象的原型对象:

console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true

在 ECMAScript 5 中可以使用 Object.getPrototypeOf() 方法,它会返回 [[prototype]] 的指向的对象:

console.log(Object.getPrototypeOf(person1) == Person.prototype);//true
console.log(Object.getPrototypeOf(person1).name);//deniro

代码在读取某个对象的属性时,会根据给定的属性名指向搜索,搜索会先从对象实例开始,如果找到就直接返回;如果没有找到,会在指针指向的原型对象进行搜索。


注意: 原型中的 constructor 属性是共享的,因此可以被对象实例访问到。


记住,在实例中添加了一个与实例原型同名的属性,这个属性就会屏蔽原型中的属性:

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "lily";
person1.sayName();//lily(来自实例)
person2.sayName();//deniro(来自原型)

使用 delete 操作符可以完全删除实例属性,这样就能够重新访问原型中的属性:

function Person() {
}

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "lily";
person1.sayName();//lily(来自实例)
person2.sayName();//deniro(来自原型)

delete person1.name;
console.log(person1.name);//deniro(来自原型)

hasOwnProperty() 方法(从 Object 继承而来),只有给定属性存在于对象实例中,才返回 true:

function Person() {
}

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));//false

person1.name = "lily";
console.log(person1.name);//lily(来自实例)
console.log(person1.hasOwnProperty("name"));//true

delete person1.name;
console.log(person1.name);//deniro(来自原型)
console.log(person1.hasOwnProperty("name"));//false

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2HdyVFQz-1588381601435)(http://p1.bpimg.com/567571/b87fb343c3e862d2.png)]

为了简便起见,图中省略了与 Person 构造函数的关系


注意: ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用于实例属性,只有在原型对象上调用 Object.getOwnPropertyDescriptor() 方法可以取得原型属性描述符。


3.2 原型与 in 操作符

单独使用 in 操作符,会把对象上能够访问的属性(实例或原型中)返回 true:

function Person() {
}

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));//false
console.log("name" in person1);//true

person1.name = "lily";
console.log(person1.name);//lily(来自实例)
console.log(person1.hasOwnProperty("name"));//true
console.log("name" in person1);//true

console.log(person2.name);//deniro(来自原型)
console.log(person2.hasOwnProperty("name"));//false
console.log("name" in person2);//true

delete person1.name;
console.log(person1.name);//deniro(来自原型)
console.log(person1.hasOwnProperty("name"));//false
console.log("name" in person1);//true

因此,同时使用 hasOwnProperty() 和 in 操作符,就可以确定属性是否存在于原型中:

function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

 function Person() {

}

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var person = new Person();
console.log(hasPrototypeProperty(person, "name"));//true

person.name = "lily";
console.log(hasPrototypeProperty(person, "name"));//false

在使用 for-in 循环时,会返回所有能够通过对象访问的、可枚举的属性(实例或原型中),注意所有的实例属性都是可枚举的,即使它们屏蔽了原型中的不可枚举属性,因此会返回(IE8 和它的早期版本是例外)。在 IE 的早期版本中存在一个 bug,即被屏蔽了原型中的不可枚举属性,不会返回:

var o = {
    toString: function () {
        return "My Object";
    }
};

for (var prop in o) {
    if (prop == "toString") {
        console.log("Found toString");//在 早期 IE 中不会显示
    }
}

使用 ECMAScript 5 的 Object.keys() 方法,可以取得对象上所有可枚举的实例属性,它接收一个对象作为参数:

function Person() {
}

Person.prototype.name = "deniro";
Person.prototype.age = 19;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
};

var keys = Object.keys(Person.prototype);
console.log(keys);//name,age,job,sayName

var p1 = new Person();
p1.name = "Jack";
p1.age = 22;
var p1keys = Object.keys(p1);
console.log(p1keys);//name,age

使用 Object.getOwnPropertyNames() 可以得到所有实例属性(不管它是否可枚举):

var keys=Object.getOwnPropertyNames(Person.prototype);
console.log(keys);//constructor,name,age,job,sayName

3.3 更简单的原型语法

可以使用一个包含所有属性和方法的对象字面量语法,简化原型对象的创建:

function Person() {

}

Person.prototype = {
    name: "deniro",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
};

注意:这个对象的 constructor 属性不再指向 Person 了。因为使用对象字面量语法完全重写了默认的 Prototype 对象,因此 constructor 属性现在直接指向 Object 构造函数:

var friend = new Person();
console.log(friend instanceof  Object);//true
console.log(friend instanceof  Person);//true
console.log(friend.constructor == Person);//false
console.log(friend.constructor == Object);//true

如果 constructor 的值很重要,可以在代码中将它设置为恰当的值:

function Person() {

}

Person.prototype = {
    constructor: Person,//重设
    name: "deniro",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
};

注意,这种方式的重设会导致它的``[[Enumerable]]` 被设置为 true,即把原型 constructor 属性默认的不可枚举变为可枚举。因此,可以使用 ECMAScript 5 的 Object.defineProperty() 来设置 constructor 属性:

//ECMAScript 5 语法
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

3.4 原型的动态性

因为在原型中查找值是一次搜索,所以对原型对象做的任何修改,都会立即从实例对象上反映出来(即使先创建实例后修改原型,也是这样):

var friend = new Person();
Person.prototype.sayHi = function () {
    console.log("hi");
};
friend.sayHi();//hi

记住,实例与原型之间是松散的连接关系,是通过指针连接的。

但如果是重写整个原型对象,就会切断构造函数与最初原型之间的联系:

function Person() {

}

var friend = new Person();
Person.prototype = {
    constructor: Person,
    name: "deniro",
    age: 19,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
};

friend.sayName();//error

在这里插入图片描述

3.5 原型对象的原型

所有的原生的引用类型(Object、Array、String 等等)都在其构造函数的原型上定义了方法:

console.log(typeof Array.prototype.sort);//function
console.log(typeof String.prototype.substring);//function

可以在原生对象的原型上定义新的方法,比如下面就演示了如何给 String 对象添加 startsWith() 方法:

String.prototype.startsWith = function (text) {
    return this.indexOf(text) == 0;
};

var msg = "Hello World!";
console.log(msg.startsWith("Hello"));//true

注意: 不建议在产品中直接修改原生对象的原型。因为如果这样做新增某个方法之后,在另一个支持该方法的实现中运行代码,就会导致命名冲突;而且这样做,也会意外重写原生方法。


3.6 原型对象的问题

原型模式中的所有实例,在默认情况下,都会取得相同的属性值,这是因为原型模式的共享的本性导致的。特别对于包含引用类型值的属性来说,问题很大:

function Person() {

}

var friend = new Person();
Person.prototype = {
    constructor: Person,
    name: "deniro",
    age: 19,
    job: "Software Engineer",
    friends: ["Jack", "Lily"],
    sayName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Bruce");
console.log(person1.friends);//"Jack","Lily","Bruce"
console.log(person2.friends);//"Jack","Lily","Bruce"
console.log(person1.friends === person2.friends);//true

实例一般都需要专属于自己的属性,因此原型模式很少单独使用。

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

使用构造函数模式定义实例的属性,使用原型模式定义共享的方法和属性,这样每个实例都拥有属于自己的一份实例属性,同时又可以共享公共方法,最后还支持向构造函数传递参数:

//构造函数模式定义实例属性
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Jack", "Lily"];
}

//原型模式定义方法和共享属性
Person.prototype = {
    constructor: Person,
    sayName: function (){
        console.log(this.name);
    }
}

var person1 = new Person("deniro", 19, "Software Engineer");
var person2 = new Person("Bruce", 29, "Doctor");

person1.friends.push("David");
console.log(person1.friends);//"Jack","Lily","David"
console.log(person2.friends);//"Jack","Lily"
console.log(person1.friends === person2.friends);//false
console.log(person1.sayName === person2.sayName);//true

组合使用构造函数模式和原型模式是目前使用最广泛、认同度最高的一种创建自定义类型的方法。

5 动态原型模式

动态原型模式把所有信息都封装在构造函数中,同时在必要的情况下才初始化原型(比如检查某个应该存在的方法是否有效,无效才添加到原型中):

function Person(name, age, job) {
    //属性
    this.name = name;
    this.age = age;
    this.job = job;

    //方法
    if (typeof this.sayName != "function") {
        Person.prototype.sayName = function () {
            console.log(this.name);
        };
    }
}

var friend = new Person("deniro", 19, "Software Engineer");
friend.sayName();

动态原型模式创建的对象,可以使用 instanceof 操作符来确定它的类型。


注意: 使用动态原型模式时,不能使用对象字面量来重新原型,因为如果在已经创建实例的情况下重写原型,就切断现有实例与新原型之间的联系。


6 寄生构造函数模式

在前面所述的模式都不适用的情况下,可以使用寄生构造函数模式。它创建一个函数,用于封装创建对象的代码,然后返回新创建的对象:

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function () {
        console.log(this.name);
    };
    return o;
}

var friend = new Person("deniro", 19, "Software Engineer");
friend.sayName();//deniro

构造函数在不返回值的情况下,默认会返回新对象实例;如果在构造函数末尾使用 return 语句,就可以重写调用构造函数的返回值。

寄生构造函数模式可以在特殊情况下为对象创建构造函数。比如,我们想创建一个具有额外方法的特殊数组,又不想直接修改 Array 构造函数,这时就可以使用这个模式:

function SpecialArray() {
    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values, arguments);//用构造函数接收到的所有参数,初始化数组

    //添加方法
    values.toPipedString = function () {
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());//red|blue|green

注意: 返回的对象与构造函数或者构造函数的原型没有关系,因此不能使用 instanceof 操作符来确定对象的类型。所以在其他模式可用的情况下,不要使用这个模式。


7 稳妥构造函数模式

稳妥对象指的是没有公共属性,而且它的方法也不引用 this 对象。稳妥对象最适合用于安全环境中(这些环境会禁用 this 和 new):

  • 新创建对象的实例方法不使用 this。
  • 不使用 new 操作符调用构造函数。
function Person(name, age, job) {
    //创建要返回的对象
    var o = new Object();

    //在此定义私有变量和函数

    //添加方法
    o.sayName = function () {
        console.log(name);
    };

    //返回对象
    return o;
}

var friend = Person("deniro", 19, "Software Engineer");
friend.sayName();//deniro

friend 中保存着一个稳妥对象,除了调用 sayName() 方法外,没有其他方法可以访问 name 属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值