JavaScript 创建对象

原文链接

乱七八糟的概念总是阻碍我们对知识更进一步的理解,所以我们先来搞清楚几个概念之间的关系。

在 JavaScript 中,引用类型的值被称为对象(或实例)。

强调:对象实例实例对象对象实例 等意。

创建一个对象

没对象怎么办?找一个呗,额,是创建一个。

初学者最常见到的就是使用这两种方法来创建单个对象:1. 使用 Object 构造函数创建,2. 使用对象字面量直接创建

其实还可以用以下的方法创建一个对象:

  • 通过构造函数来创建特定类型的对象(见后文构造函数模式)

  • 通过原型创建对象(见后文原型模式)

  • 通过 Object.create() 方法创建【MDN】

// 方法 1
var obj1 = new Object();    // 创建空对象
obj1.name = "percy";        // 为对象添加属性
obj1.getName = function(){  // 为对象添加方法
  return this.name;
};

// 方法 2
var obj2 = {
  name: "percy",
  getName: function(){
    return this.name;
  }
};

使用这两种方式创建对象有个明显的缺点:即只创建了一个特定的对象,不便于创建多个拥有相同属性和方法的不同对象。为了解决这个问题,人们便开始使用工厂模式。

工厂模式(The Factory Pattern)

  • 优点:解决了创建多个相似对象的问题

  • 缺点:无法判断工厂模式创建的对象的具体类型,因为它创建的对象都是 Object 整出来的

  • 工厂模式抽象了创建具体对象的过程

  • 由于 ES6 之前,ECMAScript 没有类(class)这个概念,所以开发人员用函数封装了以特定接口创建对象的细节。

  • ES6 中引入了类(class)这个概念,作为对象的模板。传送门

举例如下:

function Person(name,age,job){
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.job = job;
  obj.getName = function(){
    return this.name;
  };
  return obj;
}

var person1 = Person("percy",21,"killer");
var person2 = Person("zyj",20,"queen");

console.log(person1);
// Object {name: "percy", age: 21, job: "killer"}
console.log(person2);
// Object {name: "zyj", age: 20, job: "queen"}
console.log(person1.constructor);
// function Object() { [native code] }
console.log(person1.constructor);
// function Object() { [native code] }
console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // false

构造函数模式(The Constructor Pattern)

  • 优点:它可以将它创建的对象标识为一种特定的类型

  • 缺点:不同实例无法共享相同的属性或方法

constructor 属性始终指向创建当前对象的构造(初始化)函数

使用构造函数模式将前面的例子进行重写如下:

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

var person1 = new Person("percy",21,"killer");
var person2 = new Person("zyj",20,"queen");

console.log(person1);
// Object {name: "percy", age: 21, job: "killer"}
console.log(person2);
// Object {name: "zyj", age: 20, job: "queen"}
console.log(person1.constructor);
// function Person() { ... }
console.log(person1.constructor);
// function Person() { ... }
console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // true

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

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

任何函数,只要通过 new 操作符来调用,那么它就可以作为构造函数,否则就和普通函数没什么两样

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

Person("percy",22,"无");
window.getName();       // percy

从这一小节最开始的代码中,你可能注意到了,person1person2 这两个对象拥有相同的方法,但是它们相等吗?

person1.getName === person2.getName  // false

调用同一个方法,却声明了不同的对象,实在是浪费资源,所以就引进了接下来的主角:原型模式

原型模式(The Prototype Pattern)

  • 优点:它实现了不同实例可以共享属性或方法

  • 缺点:它省略了构造函数初始化参数这一环节,结果所有实例在默认情况下都取得了相同的属性值。并且如果如果原型对象中有属性的值为引用类型的,要是实例重写了这个属性,那么所有实例都会使用这个重写的属性。

  • 我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个原型对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法

    • 上面的特定类型可以是通过 new Person() 形成的 Person 类型。

好,把上面的例子改写成原型模式:

function Person(){
}

Person.prototype.name = "percy";
Person.prototype.age = 21;
Person.prototype.job = "killer";
Person.prototype.getName = function(){
  return this.name;
};

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

console.log(person1.name);   // percy
console.log(person2.name);   // percy
console.log(person1.getName === person2.getName);  // true

clipboard.png

  • 构造函数的 prototype 属性指向它的原型对象

  • 所有原型对象都具备一个 constructor 属性,这个属性指向包含 prototype 属性的函数

  • [[Prototype]] 是实例指向构造函数的原型对象的指针,目前不是标准的属性,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个 __proto__ 属性,用来实现 [[Prototype]]。

  • ECMAScript 5 增加的新方法:Object.getPrototypeOf(),它可以返回 [[Prototype]] 的值,即返回实例对象的原型。

Person.prototype.constructor === Person;              // true
person1.constructor === Person;                       // true
Object.getPrototypeOf(person1) === Person.prototype;  // true
  • 当我们访问一个对象中的属性时,首先会询问实例对象中有没有该属性,如果没有则继续查找其原型对象有没有该属性。所以要是实例对象中定义了与原型对象中相同名字的属性,则优先调用实例对象中的属性。

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";
console.log(p1.name);   // zyj
console.log(p2.name);   // percy
  • Object.prototype.hasOwnProperty(prop):检测一个属性是存在于对象实例中,还是存在于原型中,若存在于实例中,则返回 true,否则返回 false。

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";
console.log(p1.hasOwnProperty("name"));  // true
console.log(p2.hasOwnProperty("name"));  // false
  • in 操作符(prop in objectName ):判断对象实例是否能够访问某个属性(无论这个属性是自己的还是在原型对象上的),若能访问则返回 true,否则返回 false。

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";

console.log("name" in p1);  // true
console.log("name" in p2);  // true
  • Object.keys(obj):返回对象上所有可枚举的实例属性

  • Object.getOwnPropertyNames(obj):返回对象上的所有实例属性(不管能不能枚举)

var p1 = new Person();
var p2 = new Person();

p1.name = "zyj";
p1.age = 22;
Object.defineProperty(p1,"age",{
  enumerable: false
});    // 将 age 设置为不可枚举

console.log(Object.keys(p1));    // ["name"]
console.log(Object.keys(p2));    // []
console.log(Object.getOwnPropertyNames(p1)); // ["name","age"]
console.log(Object.getOwnPropertyNames(p2)); // []

console.log(Object.keys(Person.prototype));
// ["name", "age", "job", "getName"]
console.log(Object.getOwnPropertyNames(Person.prototype));
// ["constructor", "name", "age", "job", "getName"]
更简洁的原型语法

也许你已经注意到了,这一节最前面的原型写法是不是有点啰嗦,为什么每次都要写一遍 Person.prototype 呢?好,那我们现在用更简洁的原型语法如下:

function Person(){
}

Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};

是不是简洁了许多?但是这里也出现了一个问题,constructor 属性不再指向 Person了,而是指向了 Object 构造函数。记得我们在上面提到了 Person.prototype 指向的是一个对象(原型对象),而现在我们完全重写了这个原型对象,所以这个原型对象的 constructor 指向了最广泛的 Object。

var p3 = new Person();

console.log(p3 instanceof Person);  // true
console.log(p3 instanceof Object);  // true
console.log(Person.prototype.constructor === Person); // false
console.log(Person.prototype.constructor === Object); // true

所以改写上面的代码,使 constructor 指向 Person:

function Person(){
}

Person.prototype = {
  constructor: Person,         
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};

注意,以这种方式重设constructor 属性会导致它的 [[Enumerable]] 特性被设置为 false,从而 constructor 属性变得可以枚举了,但是原生的 constructor 属性是不可枚举的,所以我们利用 Object.defineProperty() 再改写一下代码:

function Person(){
}

Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});
var p3 = new Person();

console.log(p3 instanceof Person);  // true
console.log(p3 instanceof Object);  // true
console.log(Person.prototype.constructor === Person); // true
console.log(Person.prototype.constructor === Object); // false
  • 重写原型对象应该在创建实例之前完成,否则会出现不可预知的错误

function Person(){
}
var p3 = new Person();

Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

p3.getName(); // 报错,TypeError: p3.getName is not a function(…)
  • 当原型对象中有属性的值为引用类型时...

function Person(){
}
Person.prototype = {
  name: "percy",
  age: 21,
  job: "killer",
  friends: ['zyj','Shelly','Dj Aligator'],  // 添加
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

var p1 = new Person();
var p2 = new Person();

p1.job = "programmer";
p1.friends.push('Mary','Iris');

console.log(p1.job);    // programmer
console.log(p2.job);    // killer
console.log(p1.friends);
// ["zyj", "Shelly", "Dj Aligator", "Mary", "Iris"]
console.log(p2.friends);
// ["zyj", "Shelly", "Dj Aligator", "Mary", "Iris"]
console.log(p1.friends === p2.friends);  // true

console.log(Person.prototype.friends);
// ["zyj", "Shelly", "Dj Aligator", "Mary", "Iris"]

看出问题来了吗?当原型对象中有属性的值为引用类型时,要是一个实例重写了这个属性,那么所有的实例都会使用这个重写后的属性。要是还不了解的话,可以看看我以前的文章,谈的是基本类型和引用类型在内存中的存储方式,以及改变它们的值时,内存中是如何变化的。

组合使用构造函数模式和原型模式(Combination Constructor/Prototype Pattern)

  • 原理:构造函数模式用于实例自己的属性,而原型模式用于定义方法和需要共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['zyj'];
}
Person.prototype = {
  getName: function(){
    return this.name;
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

var person1 = new Person("percy","21","killer");
var person2 = new Person("Bob","26","developer");

person1.friends.push("Iris","Alice");

console.log(person1.name);      // percy
console.log(person2.name);      // Bob
console.log(person1.friends);   // ["zyj", "Iris", "Alice"]
console.log(person2.friends);   // ["zyj"]

console.log(person1.friends === person2.friends); // false
console.log(person1.getName === person2.getName); // true

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

  • 为上面的代码补一张图吧 :)!

啊啊啊

动态原型模式(Dynamic Prototype Pattern)

  • 原理:将所有信息封装到构造函数中。

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['zyj'];
  if(typeof this.getName != "function" ){
    Person.prototype.getName = function(){
      return this.name;
    };
    Person.prototype.getJob = function(){
      return this.job;
    };
  }
}

var person = new Person("percy",21,"programmer");
console.log(person.getName()); // percy
console.log(person.getJob());  // programmer

将所有信息封装到构造函数里,很完美,有木有?

  • 这里使用 if 语句检查原型方法是否已经初始化,从而防止多次初始化原型方法。

  • 这种模式下,不能使用对象自面量重写原型对象。因为在已经创建了实例的情况下再重写原型对象的话,会切断现有实例与新原型对象之间的联系。

  • 看这里,有更详细的对上面代码的解释,链接

寄生构造函数模式(Parasitic Constructor Pattern)

似曾相识哈!

一句话阐明:除了使用 new 操作符并把包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。

function Person(name,age,job){
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.job = job;
  obj.getName = function(){
    return this.name;
  };
  return obj;
}

var person1 = new Person("percy",21,"killer");
var person2 = new Person("zyj",20,"queen");

person1.getName();   // percy
person2.getName();   // zyj
  • 建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式(Durable Constructor Pattern)

  • 稳妥构造函数遵循与寄生构造函数类似的模式,但是有 2 点不同:

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

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

function Person(name,age,job){
  var obj = new Object();

  // 可以在这里定义私有变量和函数

  obj.getName = function(){
    return name;
  };
  return obj;
}

var person1 = new Person("percy",21,"killer");
var person2 = new Person("zyj",20,"queen");

person1.getName();   // percy
person2.getName();   // zyj

注意,在这种模式下创建的对象中,除过调用 getName() 方法外,没有其他方法访问 name 的值。

我想问个问题,最后的这个模式可以用在哪些地方呢?希望有经验的朋友解答一下。

参考资料

  • 【书】《JavaScript 高级程序设计(第三版)》


hjxja890980
962 声望54 粉丝

nothing.