JS创建对象的几种方式

本文深入探讨JavaScript中的对象创建模式,包括工厂模式、构造函数模式和原型模式。通过示例解释了每种模式的原理、优缺点及组合使用方式,揭示了在实例化对象时如何共享属性和方法,以及如何处理引用类型的属性共享问题。重点讨论了构造函数和原型在实现继承中的角色,并展示了如何通过组合模式优化对象创建过程。

目录

JavaScript工厂模式

 构造函数模式

 原型模式

组合模式


无序属性的集合,其属性可以包含基本值,对象,或者函数。可以将对象想象成散列表:键值对,其中值可以是数据或者函数。ECMAScript中的对象其实就是一组数据(属性)和功能(方法)的集合。

 字面量模式

var obj = {
  name: "bry",
  age: 13,
  sayName:function(){
    console.log("my name is ",this.name);
  }
}

JavaScript工厂模式

 使用工厂模式创建对象

//将创建对象的代码封装在一个函数中
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');

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

缺点:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。

 构造函数模式

  JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不过可以用来创建对象。

 自定义构造函数

// 自定义构造函数
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

在这个案例中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。

  • 属性和方法直接赋值给了 this。

  • 没有 return。

  • 另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是能创建对象的函数。

 构造函数的问题

构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的案例而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例。我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对象。

 原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

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

 这里,所有属性和 sayName()方法都直接添加到了 Person 的 prototype 属性上,构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。

更简单的原型模式

在前面的案例中,每次定义一个属性或方法都会把 Person.prototype 重写一遍。为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法:

function Person() {}
Person.prototype = {
  name: "zhangsan",
  age: 29,
  gender: "male",
  sayName() {
    console.log(this.name);
  }
};
// 在这个案例中,Person.prototype 被设置为等于一个通过对象字面量创建的新对象。最终结果是一样的,只有一个问题:这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了。在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。
var person1 = new Person()
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true

那怎么解决这个问题呢?可以在重写原型对象时,专门设置constructor的值;但是,以这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。而原生 constructor 属性默认是不可枚举的。因此,如果你使用的是兼容 ECMAScript 的 JavaScript 引擎,那可能会改为使用 Object.defineProperty()方法来定义 constructor 属性:

function Person() { }
Person.prototype = {
  //这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性
  //constructor: Person,
  name: "zhangsan",
  age: 29,
  gender: "male",
  sayName() {
    console.log(this.name);
  }
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});
var person1 = new Person()
console.log(person1.constructor == Person); //true
console.log(person1.constructor == Object); //false

原型的问题

    原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。

   我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面案例中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。来看下面的案例:

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");
console.log(person1.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person2.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person1.friends === person2.friends); // true

这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建了两个Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。

组合模式

组合使用构造函数模式和原型模式。构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.firends = ['zhangsan', 'lisi'];
}
Person.prototype = {
  constructor: Person,
  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

### JavaScript创建对象的常见方式 JavaScript 提供了多种创建对象的方法,每种方法都有其特定的应用场景和优缺点。以下是常见的九种创建对象方式及其示例: #### 1. 工厂模式 通过定义一个函数来封装对象的创建逻辑。 ```javascript function createPerson(name, age) { return { name: name, age: age, sayHello: function() { console.log("Hello, I'm " + this.name); } }; } var person = createPerson("Alice", 25)[^1]; ``` #### 2. 构造函数模式 使用构造函数创建对象实例。 ```javascript function Person(name, age) { this.name = name; this.age = age; this.sayHello = function() { console.log("Hello, I'm " + this.name); }; } var person = new Person("Bob", 30); ``` #### 3. 原型模式 利用原型属性共享方法,减少内存占用。 ```javascript function Person() {} Person.prototype.name = "Charlie"; Person.prototype.age = 28; Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.name); }; var person = new Person(); ``` #### 4. 组合构造函数与原型模式 结合构造函数和原型的优点,既可以在实例上保存私有数据,又可以共享方法。 ```javascript function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.name); }; var person = new Person("David", 35); ``` #### 5. 动态原型模式 动态检测并初始化原型中的方法。 ```javascript function Person(name, age) { this.name = name; this.age = age; if (typeof this.sayHello !== 'function') { Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.name); }; } } var person = new Person("Eve", 27); ``` #### 6. 寄生构造函数模式 返回一个新的对象而不是 `this` 对象。 ```javascript function Person(name, age) { var obj = {}; obj.name = name; obj.age = age; obj.sayHello = function() { console.log("Hello, I'm " + this.name); }; return obj; } var person = new Person("Frank", 40)[^1]; ``` #### 7. 稳定寄生组合模式 结合寄生构造函数和原型继承的优势。 ```javascript function Person(name, age) { var obj = Object.create(Person.prototype); obj.name = name; obj.age = age; return obj; } Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.name); }; var person = new Person("Grace", 32); ``` #### 8. ES6 类语法 ES6 引入类的概念简化了面向对象编程。 ```javascript class Person { constructor(name, age) { this.name = name; this.age = age; } sayHello() { console.log("Hello, I'm " + this.name); } } var person = new Person("Hannah", 29); ``` #### 9. 使用 `Object.create` 通过指定原型链手动创建对象。 ```javascript const prototypeObj = { sayHello: function() { console.log("Hello, I'm " + this.name); } }; var person = Object.create(prototypeObj, { name: { value: "Ian", writable: true }, age: { value: 33, writable: true } }); ``` 以上是 JavaScript 中常用的几种创建对象方式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值