JavaScript 建立对象

JavaScript 建立对象

原文连接javascript

乱七八糟的概念老是阻碍咱们对知识更进一步的理解,因此咱们先来搞清楚几个概念之间的关系。java

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

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

建立一个对象

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

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

其实还能够用如下的方法建立一个对象:设计

  • 经过构造函数来建立特定类型的对象(见后文构造函数模式)指针

  • 经过原型建立对象(见后文原型模式)

  • 经过 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 高级程序设计(第三版)》

相关文章
相关标签/搜索