网上关于创建对象的博文已经烂大街了,写的比我好的也已经烂大街了,那我为什么还要写这篇博文呢,因为我发现有些东西写下来、敲出来才是真的理解了。至少这篇文章对我来说意义重大。
搜了下网上的创建对象的几种方法:有条回答获赞数比较高而且十分简洁:熟读 《JavaScript 高级程序设计》第六章。
简单粗暴,我就直接去翻尘封已久的犀牛书。
本文参考《JavaScript 高级程序设计(第三版)》,大部分代码 Copy,Copy 是抄写,不是复制粘贴;部分文字带有自己的理解。
先总结下读完这一章后引出的,我认为比较重要的知识点:
- 对象的属性类型:数据属性和访问器属性
- new 方法做了什么
- 基本数据类型和引用数据类型
- 栈内存与堆内存
——————————————————————————————————————————————————
正文:
面向对象的语言都有一个标志,即类。
上帝根据自己的形象造男造女,这里的上帝便是类,男男女女便是对象。
在对象中,每一个属性和方法都已一个名字,而每个名字都映射到一个值,即 ECMAScript 中的对象无非就是一组名值对,其中值可以是数据或者函数。
创建对象的方法
- 最简单粗暴:对象字面量
var person = new Object();
persion.name = 'Bob';
persion.age = 24;
persion.job = 'Software Engineer'
persion.sayName = function(){
consolo.log(this.name)
}
var person = {
name: 'Bob',
age: 24,
job: 'Software Engineer',
sayName: function(){
consolo.log(this.name)
}
}
- 工厂模式
function createDevice(name,userId,job){
var obj = new Object();
obj.name = name;
obj.userId = userId;
obj.job = job
obj.sayName = function(){
console.log(this.name)
return this.name
}
return obj
}
var device1 = createDevice('device1',1,'light')
console.log(device1) // { name: 'device1', userId: 1, job: 'light', sayName: [Function] }
console.log(device1 instanceof Object) // true
- 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}; }
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1) // Person {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }
console.log(person1 instanceof Object) // true,
创建 Person 实例,必须使用
new
操作符,这种方式实际上经历了以下四个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此
this
就指向了这个新对象)- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
缺点: 每个方法都要在每个实例上重新创建一遍,所有实例内部的 Function
都不是一个同一个 Function
,(注: 因为在 JS 中,一切皆对象,嗯,联想到了函数声明,每定义一个函数也就是实例化一个对象,逻辑角度是等价的):
console.log(person1.sayName === person2.sayName) // false
- 原型模式
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
console.log(person1.sayName === person2.sayName) // true
这样就不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中
无论什么时候:
- 只要创建了函数,就会根据函数特定的规划为函数创建一个
prototype
属性,这个属性指向函数的原型对象- 原型对象自动获得一个
constructor
属性,这个属性包含一个指向prototype
属性所在函数的指针
但是,为什么说这样的方式可以避免出现像构造函数那样的问题呢?
原来是这样:在调用 person1.sayName()
的时候,解析器首先会问:
- “实例
person1
有sayName
属性吗?” 答:“没有。" - 然后,它继续搜索,再问:“
person1
的原型有sayName
属性吗? ”答:“有。” - 于是,它就读取那个保存在原型对象中的函数。当我们调用
person2.sayName()
时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理
对 JS
对象的理解好像又深刻了,我又尝试以下代码:
console.log(Person.prototype)
// Person {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }
console.log(person1.prototype)
// undefined
console.log(person1.constructor.prototype)
// Person {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }
所以:我以前对Person.prototype
的理解是:它是函数的原型对象,现在看来,这种理解是不够准确的。
其实:
-
prototype
只是每个函数在被创建时自带的一个属性,这个属性指向函数的原型对象, -
从而可以使用
person1.constructor
获取原型对象中的constructor
属性, -
而
construtor
指向的是Person
-
所以
person1.constructor.prototype
也不难理解
(好吧,刚开始真的有点绕,多读几次其实还好)
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这
个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性,举例如下
person1.name = "Greg";
alert(person1.name); // "Greg"——来自实例
alert(person2.name); // "Nicholas"——来自原型
delete person1.name;
alert(person1.name); // "Nicholas"——来自原型
// 注意理解是屏蔽,我的理解也就是优先级是与作用域保持一致,但是只是屏蔽,并没有改变原型中属性的的值。
有个方法可以判断一个属性是存在于实例中,还是存在于原型中。
person1.name = "Greg";
alert(person1.name); // "Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); // "Nicholas"——来自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); // "Nicholas"——来自原型
alert(person1.hasOwnProperty("name")); //false
- 原型模式进阶
function Person(){}
Person.prototype = {
name : " Nicholas ",
age : 29,
friends : ["Shelby", "Court"],
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
原型模式的缺点:
如果原型对象上面的属性所对应的值是引用类型,那么问题就来了。众所周知,对于 JS
的数据类型分为两类:**基本类型 **和 引用类型,我所理解的他们的区别主要是存储的空间不同:
- 基本数据类型存在于栈内存,键值对的方式存储
- 对于引用数据类型,栈内存中存的是键和引用地址,而引用地址指向的是堆内存中该对象所存储的地方
所以:
person1.friends.push("Ayden");
alert(person1.friends); //"Shelby,Court,Ayden"
alert(person2.friends); //"Shelby,Court,Ayden"
alert(person1.friends === person2.friends); //true
因为它们的引用地址指向的是同一个堆内存对象(数组),所以,每个实例一般都是有专属于自己的属性的。
书中是这样说的:
假如我们的初衷就是像这样:
在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部
属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
- 组合使用构造函数模式和原型模式(最广泛)
function Person(name, age, job){
this.name = name; 3 this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
2
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Ayden");
alert(person1.friends); // "Shelby,Count,Ayden"
alert(person2.friends); // "Shelby,Count"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true
实例所有的属性都用构造函数定义,所有的方法以及 constructor
属性都早原型对象中定义。
都这样做的好处就是确保每个实例的属性都是自己独立的,但是共享了对方法的引用,最大限度的节省了内存空间。
- 动态原型模式
function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
然后突然有个问题困惑了,直接上代码:
// 代码 1
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName) // false
// 代码 2
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName) // true
论封装性,代码一不是更好吗?为什么都是 prototype
属性的重写,代码一和代码二的结果却不一样?
我想到了 new
关键字做了什么:创建对象,指针指向,执行函数,返回对象,我应该是忽略了执行函数这一步。
代码一中,每次实例化对象,prototype
属性都进行了重写,重写了两次,所以改变了现有实例与新原型之间的联系。
而代码二中,虽然同样是两次实例化,但不同的是全局代码也就是给 Person
的原型定义只执行了一次,所以 sayName
方法的引用指向的是同一个堆内存对象。
书中是这样写的:
使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
我犯得就是这个错误。
还有两种创建对象的模式,寄生构造函数模式和稳妥构造函数模式,我工作中还没接触过,只看了看原理,没有去深入理解。
理解以下这些知识点对理解创建对象比较有帮助:
- 对象的属性类型:数据属性和访问器属性
- new 方法做了什么
- 基本数据类型和引用数据类型
- 栈内存与堆内存
以上是我在读第 6 章第一节和第二节后的笔记以及收获,有什么不对的地方还麻烦大家提出,互相讨论,感激。