初始JavaScript-创建对象

本文探讨了JavaScript中创建对象的各种方法,包括Object构造函数、对象字面量及其缺点,然后深入介绍了工厂模式、构造函数模式、原型模式。在原型模式中,讲解了原型对象的理解、原型与in操作符的关系以及原型动态性的特点。同时,文章还讨论了组合使用构造函数模式和原型模式,以及如何应对原型模式的缺点,如动态原型模式、寄生构造函数模式和稳妥构造函数模式的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Objec构造函数

通过Object构造函数创建对象是最简单的方法。

var person = new Object();
person.name = "Jack";
person.age = 29;
person.sayName = function(){
    alert(this.name);
};

对象字面量

对象字面量成为创建这种对象的首选模式。

var person = {
    name:"Mike",
    age:29,
    sayName:function() {
        alert(this.name);
    }
};

以上两种方法的缺点

使用同一个接口创建很多对象,会产生大量的重复代码。

工厂方法

因为ECMAScript无法创建类,所以就用函数来封装以特定接口对象的细节。

function createPerson(name, age){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function(){
    alert(this.name);
};
    return o;
}
var person1 = createPerson("Nicholas", 29);
var person2 = createPerson("Greg", 27);

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

function Person(name, age){
    this.name = name;
    this.age = age; 
    this.sayName = function(){
    alert(this.name);
    };
}
var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

之前创建的自定义的构造函数,可以通过下列任何一种方式来调用:

// 当作构造函数使用
var person = new Person("Nicholas", 29);
person.sayName(); //"Nicholas"

// 作为普通函数调用
Person("Greg", 27); // 添加到 window
window.sayName(); //"Greg"

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

当作为普通函数调用时,其属性和方法就被添加到windows对象上,所以就需要windows来调用。

构造函数主要缺点就是每个方法都要在实例上重新创建一遍。

上面构造函数中函数与this.sayName= new function("alert (this.name)");等价。以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建 Function 新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的。

alert(person1.sayName == person2.sayName);//false

解决方法是函数也是对象,所以把函数定义转移到构造函数外部来解决。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);

由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName()函数。

可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

原型模式

创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。原型模式就是不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

function Person(){
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"

var person2 = new Person();
person2.sayName(); //"Nicholas"

alert(person1.sayName == person2.sayName); //true

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。

创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。 这个指针叫[[Prototype]],只存在于实例和构造函数的原型对象之间。

这里写图片描述

可以通过 isPrototypeOf()方法来确定对象之间是否存在[[Prototype]]。如果[[Prototype]]指向调用 isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回 true。

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。

alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"

支持这个方法的浏览器有 IE9+、 Firefox 3.5+、 Safari 5+、 Opera 12+和 Chrome。

当程序读取到某个属性时,会从对象实例本身开始寻找。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

不能通过实例重写原型的值。如果给实例添加了一个原型中属性同名的一个属性,则会屏蔽原型中的属性,只显示实例中的属性。如下例:

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
alert(this.name);
};

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

person1.name = "Greg";
alert(person1.name); //"Greg"—— 来自实例
alert(person2.name); //"Nicholas"—— 来自原型

不过,使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

delete person1.name;
alert(person1.name); //"Nicholas"—— 来自原型

使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回 true。

//原型模式与之前相同
alert(person1.hasOwnProperty("name")); //false

person1.name = "Greg";
alert(person1.name); //"Greg"—— 来自实例
alert(person1.hasOwnProperty("name")); //true

alert(person2.name); //"Nicholas"—— 来自原型
alert(person2.hasOwnProperty("name")); //false

delete person1.name;
alert(person1.name); //"Nicholas"—— 来自原型
alert(person1.hasOwnProperty("name")); //false

原型与 in 操作符

在单独使用时, in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

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

person1.name = "Greg";
alert(person1.name); //"Greg" —— 来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" —— 来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true

delete person1.name;
alert(person1.name); //"Nicholas" —— 来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true

同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。返回true,表示存在于原型中。返回false,表示存在于实例中。

var person = new Person();//存在于原型中
alert(hasPrototypeProperty(person, "name")); //true

person.name = "Greg";//存在于实例中
alert(hasPrototypeProperty(person, "name")); //false

更简单的原型语法

更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。

function Person(){
}
Person.prototype = {
    name : "Nicholas",
    age : 29,
    sayName : function () {
        alert(this.name);
    }
};

但是,constructor属性不再指向Person了。每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性 (指向 Object 构造函数),不再指向 Person 函数。

var friend = new Person();

alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true

从上面例子可以看出,实例的constructor属性指向了Object属性。如果constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。

function Person(){
}
Person.prototype = {
    constructor:Person, 
    name : "Nicholas",
    age : 29,
    sayName : function () {
        alert(this.name);
    }
};

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。

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

因为,首先会从实例中查找,没有就会去原型中查找,所以能立即反映出来。

但是如果重写了原型,等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。

function Person(){
}
var friend = new Person();
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName(); //error



重写原型对象后,新的原型对象与原来存在的实例之间不存在联系,所以访问出错。

原生对象的原型

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。

原型对象的缺点

它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,会在某种程度上带来一些不方便。

原型模式的最大问题是由其共享的本性所导致的。对于包含引用类型的属性,如果更改原型的属性,所有实例都会更改,然而有时候要求实例拥有自己的属性,这就会出现问题。

function Person(){
}
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(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, job){
    this.name = name;
    this.age = age;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}
var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);

person1.friends.push("Van");

alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"

alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。

动态原型模式

动态原型模式是可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

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

    //方法
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}
var friend = new Person("Nicholas", 29);
friend.sayName();

方法那段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。

对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。

使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系

寄生构造函数模式

在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。

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

var friend = new Person("Nicholas", 29);
friend.sayName(); //"Nicholas"

此方法适合于那些不能直接修改构造函数,且还需要添加新属性或新方法的函数。

构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

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

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。

function Person(name, age, job){
    //创建要返回的对象
    var o = new Object();
    //可以在这里定义私有变量和函数
    //添加方法
    o.sayName = function(){
    alert(name);
    };
    //返回对象
    return o;
}

注意,在以这种模式创建的对象中, 除了使用 sayName()方法之外,没有其他办法访问 name 的值。

参考资料

《JavaScript高级程序设计》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值