资讯专栏INFORMATION COLUMN

JavaScript 创建对象

Leo_chen / 843人阅读

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

JavaScript 创建对象

原文链接

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

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

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

实例 百度百科

What is the difference between an Instance and an Object?

创建一个对象

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

初学者最常见到的就是使用这两种方法来创建单个对象: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;
  }
};

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

扩展链接:

What is the difference between new Object() and object literal notation?

[Why use {} instead of new Object() and use [] instead of new Array() and true/false instead of new Boolean()?](http://stackoverflow.com/ques...

工厂模式(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

构造函数的 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 高级程序设计(第三版)》

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80295.html

相关文章

  • JavaScript中的面向对象(object-oriented)编程

    摘要:对象在中,除了数字字符串布尔值这几个简单类型外,其他的都是对象。那么在函数对象中,这两个属性的有什么区别呢表示该函数对象的原型表示使用来执行该函数时这种函数一般成为构造函数,后面会讲解,新创建的对象的原型。这时的函数通常称为构造函数。。 本文原发于我的个人博客,经多次修改后发到sf上。本文仍在不断修改中,最新版请访问个人博客。 最近工作一直在用nodejs做开发,有了nodejs,...

    JerryZou 评论0 收藏0
  • [译] V8 使用者文档

    摘要:注意句柄栈并不是调用栈中的一部分,但句柄域却在栈中。一个依赖于构造函数和析构函数来管理下层对象的生命周期。对象模板用来配置将这个函数作为构造函数而创建的对象。 如果你已经阅读过了上手指南,那么你已经知道了如何作为一个单独的虚拟机使用 V8 ,并且熟悉了一些 V8 中的关键概念,如句柄,域 和上下文。在本文档中,还将继续深入讨论这些概念并且介绍其他一些在你的 C++ 应用中使用 V8 的...

    lei___ 评论0 收藏0
  • JavaScript对象

    摘要:对象的分类内置对象原生对象就是语言预定义的对象,在标准定义,有解释器引擎提供具体实现宿主对象指的是运行环境提供的对象。不过类型是中所有类型的父级所有类型的对象都可以使用的属性和方法,可以通过的构造函数来创建自定义对象。 对象 javaScript中的对象,和其它编程语言中的对象一样,可以比照现实生活中的对象来理解。在JavaScript中,一个对象可以是一个单独拥有属性和类型的实体。和...

    xavier 评论0 收藏0
  • JavaScript基础之创建对象、原型、原型对象、原型链

    摘要:在最开始的时候,原型对象的设计主要是为了获取对象的构造函数。同理数组通过调用函数通过调用原型链中描述了原型链的概念,并将原型链作为实现继承的主要方法。 对象的创建 在JavaScript中创建一个对象有三种方式。可以通过对象直接量、关键字new和Object.create()函数来创建对象。 1. 对象直接量 创建对象最直接的方式就是在JavaScript代码中使用对象直接量。在ES5...

    wangbjun 评论0 收藏0
  • JavaScript 工厂函数 vs 构造函数

    摘要:当谈到语言与其他编程语言相比时,你可能会听到一些令人困惑东西,其中之一是工厂函数和构造函数。好的,让我们用构造函数做同样的实验。当我们使用工厂函数创建对象时,它的指向,而当从构造函数创建对象时,它指向它的构造函数原型对象。 showImg(https://segmentfault.com/img/bVbr58T?w=1600&h=900); 当谈到JavaScript语言与其他编程语言...

    RayKr 评论0 收藏0
  • JavaScript学习笔记(二) 对象与函数

    摘要:在中函数是一等对象,它们不被声明为任何东西的一部分,而所引用的对象称为函数上下文并不是由声明函数的方式决定的,而是由调用函数的方式决定的。更为准确的表述应该为当对象充当函数的调用函数上下文时,函数就充当了对象的方法。 引言:当理解了对象和函数的基本概念,你可能会发现,在JavaScript中有很多原以为理所当然(或盲目接受)的事情开始变得更有意义了。 1.JavaScript...

    jeffrey_up 评论0 收藏0

发表评论

0条评论

Leo_chen

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<