JavaScript创建对象的几种方式

本文详细介绍了JavaScript中创建对象的几种方式,包括Object()、字面量方式、工厂模式、自定义构造函数模式和原型模式。讨论了每种模式的优缺点,如工厂模式解决了重复实例化问题,但无法识别对象类型,而构造函数虽能标识类型,但方法会在每个实例上重新创建。此外,还讲解了原型模式如何实现属性和方法的共享,以及组合模式如何结合构造函数和原型的优点。

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

一、Object()创建对象、用字面量的方式创建对象

1.Object()创建对象

new Object()就是利用JavaScript提供的构造函数实例化了一个对象

var person = new Object();
//为这个实例化的对象添加属性
person.name = "zhangsan";
person.age =21;
person.gender = 'male';
person.sayName = function(){
    console.log(this.name)
}

2.用字面量的方式创建对象

var person = {
  name: "zhangsan",
  age: 18,
  gender: 'male',
  sayName: function(){
      console.log(this.name);
  }
}

缺点:以上两种方法在使用同一接口创建多个对象时,会产生大量重复代码,为了解决此问题,工厂模式被开发。

二、工厂模式创建对象

//将创建对象的代码封装在一个函数中
function createPerson(name, age, gender) {
  var person = new Object();
  person.name = name;
  person.age = age;
  person.gender = gender;
  person.sayName = function () {
    console.log(this.name);
  }
  return person;
}
//利用工厂函数来创建对象
var person1 = createPerson("zhangsan", 18, 'male');
var person2 = createPerson("lisi", 20, 'female');

优点:只要我们往工厂函数里面塞参数,工厂函数就会像生产产品一样造出实例来。

缺点:工厂模式解决了重复实例化多个对象的问题,但没有解决对象识别的问题(无从识别对象的类型,因为全部都是Object,本例中,得到的都是person对象,对象的类型都是Object)。

三、自定义构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

前面的案例使用构造函数可以这样写

// 自定义构造函数
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function () {
    console.log(this.name);
  }
}

var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');

person1.sayName(); // zhangsan
person2.sayName(); // lisi

注意:1、按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从  面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。

           2、任何函数,只要通过new操作符来调用,那它就可以作为构造函数;如果不用new操作符来调用,它就是一个普通函数的调用。比如,前面的案例中定义的 Person()可以像下面这样调用:

var Person = function (name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function () {
    console.log(this.name);
  };
}
// 作为构造函数
var person = new Person("Jacky", 29, "male");
person.sayName(); // Jacky
// 作为函数调用
Person("lisi", 27, "female"); // 添加到全局对象 node中是global 浏览器中是window
global.sayName(); // lisi
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "wangwu", 25, "male");
o.sayName(); // wangwu

这个案例展示了典型的构造函数调用方式,即使用 new 操作符创建一个新对象。然后是普通函数的调用方式。

这时候没有使用 new 操作符调用 Person(),结果会将属性和方法添加到全局对象。这里要记住,在调用一个函数而没有明确设置 this 值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this 始终指向 Global 对象(在浏览器中就是 window 对象,在node中是global)。因此在上面的调用之后,Global 对象上就有了一个 sayName()方法,调用它会返回"lisi"。

最后的调用方式是通过 call()(或 apply())调用函数,同时将特定对象指定为作用域。这里的调用将对象 o 指定为 Person()内部的 this 值,因此执行完函数代码后,所有属性和 sayName()方法都会添加到对象 o 上面。

          

       3、如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数

对比工厂模式有以下不同之处:

1、没有显式地创建对象

2、直接将属性和方法赋给了 this 对象

3、没有 return 语句

以此方法调用构造函数步骤:

1、在内存中创建一个新对象

2、将构造函数的作用域赋给新对象(将this指向这个新对象)

3、执行构造函数代码(为这个新对象添加属性)

4、如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

可以看出,构造函数知道自己从哪里来(通过 instanceof 可以看出其既是Object的实例,又是Person的实例),一般认为 instanceof 操作符是确定对象类型更可靠的方式。instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。或者说判断一个对象是某个对象的实例。

//instanceof 操作符的结果所示:
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
console.log(person2 instanceof Object); // true 
console.log(person2 instanceof Person); // true 

构造函数的问题:

构造函数虽然好用,但也不是没有缺点,使用构造函主要问题就是每个方法都要在每个实例上重新创建一遍,前边的person1和person2虽然都有一个sayName的方法,但是那两个方法不是同一个function的实例。构造函数内的方法在做同一件事,但是实例化后却产生了不同的对象,函数也是对象,因此每次定义函数时,都会初始化一个对象 。

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

因为都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。

要解决这个问题,可以把函数定义转移到构造函数外部:

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = sayName;
}
function sayName() {
  console.log(this.name);
}
var person1 = new Person("zhangsan", 29, "male");
var person2 = new Person("lisi", 27, "female");
person1.sayName(); // zhangsan 
person2.sayName(); // lisi

自定义构造函数模式的缺点:在这个例子中,我们把sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName属性设置成等于全局的sayName函数。这样一来,由于sayName包含的是一个指向外部函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是新的问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,这会导致自定义类型引用的代码不能很好地聚集一起,自定义的引用类型就丝毫没有封装性可言了。

优点:相比于工厂模式,定义自定义构造函数可以确保实例被标识为特定类型,这是一个很大的好处。在这个案例中,person1 和 person2 除了是Person类型外,也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。

四、原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。按字面意思解释,prototype就是通过该构造函数创建的某个实例的原型对象,但是其实prototype是每个构造函数的属性而已。

原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

如下所示:

与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的sayName()函数。

function Person(){}//声明一个构造函数
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};
var person1 = new Person();
person1.sayName(); // zhangsan 
var person2 = new Person();
person2.sayName(); // zhangsan 
console.log(person1.sayName == person2.sayName); // true

原型层级:

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。因此,在调用 person1.sayName()时,会发生两步搜索。首先,JavaScript 引擎会问:“person1 实例有 sayName 属性吗?”答案是没有。然后,继续搜索并问:“person1 的原型有 sayName 属性吗?”答案是有。于是就返回了保存在原型上的这个函数。在调用 person2.sayName()时,会发生同样的搜索过程,而且也会返回相同的结果。这就是原型用于在多个对象实例间共享属性和方法的原理。虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。

下面看一个案例:

function Person() { }
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
console.log(person2.name); // zhangsan,来自原型
console.log(person1.hasOwnProperty('name')); //true
console.log(person2.hasOwnProperty('name')); //false

在这个案例中,person1 的 name 属性遮蔽了原型对象上的同名属性。虽然 person1.name 和person2.name 都返回了值,但前者返回的是"lisi"(来自实例),后者返回的是"zhangsan"(来自原型)。当 console.log()访问 person1.name 时,会先在实例上搜索个属性。因为这个属性在实例上存在,所以就不会再搜索原型对象了。而在访问 person2.name 时,并没有在实例上找到这个属性,所以会继续搜索原型对象并使用定义在原型上的属性。我们也可以通过hasOwnProperty()可以查看访问的是实例属性还是原型属性。只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为 null,也不会恢复它和原型的联系。不过,使用delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。

function Person() { }
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
// 通过hasOwnProperty()可以查看访问的是实例属性还是原型属性
console.log(person1.hasOwnProperty('name')); //false

person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
//只在重写 person1 上 name 属性的情况下才返回 true,表明此时 name 是一个实例属性,不是原型属性
console.log(person1.hasOwnProperty('name')); //true

console.log(person2.name); // zhangsan,来自原型

console.log(person2.hasOwnProperty('name'));//false

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

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

这个修改后的案例中使用 delete 删除了 person1.name,这个属性之前以"lisi"遮蔽了原型上的同名属性。然后原型上 name 属性的联系就恢复了,因此再访问 person1.name 时,就会返回原型对象上这个属性的值。

原型模式优点:所有的对象实例都可以共享它包含的属性和方法。

缺点:它省略了为构造函数传递初始化参数这一环节,结果会导致所有实例默认都取得相同的属性值。这还不是最大问题,原型模式的最大问题是由其共享的本性所导致的。我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面案例中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。

function Person() { }
Person.prototype = {
  constructor: Person,
  name: "zhangsan",
  friends: ["lisi", "wangwu"],
  sayName() {
    console.log(this.name);
  }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("zhaoliu");
person1.name='lisi';
console.log(person1.name);//lisi
console.log(person2.name);//zhangsan
console.log(person1.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person2.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person1.friends === person2.friends); // true
console.log(person1.name === person2.name); // false

这里,person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。

五、组合模式

组合模式是构造函数模式和原型模式的结合,集这两个模式的优点于一身,同时弥补了它们的不足。构造函数用于定义实例属性,原型模式用于定义方法和共享属性。

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.firends = ['zhangsan', 'lisi'];
}
Person.prototype = {
  constructor: Person,//每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
  sayName: function () {
    console.log(this.name);
  }
};
var p1 = new Person('larry', 44, 'male');
var p2 = new Person('terry', 39, 'male');

p1.firends.push('robin');
console.log(p1.firends); // [ 'zhangsan', 'lisi', 'robin' ]
console.log(p2.firends); // [ 'zhangsan', 'lisi' ]
console.log(p1.firends === p2.firends); // false
console.log(p1.sayName === p2.sayName); // true

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值