1. 对象的定义
面向对象的语言都有一个标志,即类。
定义:对象是 JavaScript 的一个基本数据类型,是一种复合值,它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值。即属性的无序集合
例:上帝根据自己的形象造男造女,这里的上帝便是类,这里的 男和女 便是对象。
在对象中,每一个属性和方法都已一个名字,而每个名字都映射到一个值,即 ECMAScript 中的对象无非就是一组名值对,其中值可以是数据或者函数。
2. 对象的属性
在 JavaScript 中,对象的属性分为两种类型:数据属性和访问器属性。
2.1 数据属性
1. 数据属性:包含的是一个数据值的位置,在这可以对数据值进行读写。
2. 数据属性包含四个特性,分别是:
属性 | 含义 |
---|---|
configurable | 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为 true |
enumerable | 表示能否通过 for-in 循环返回属性 |
writable | 表示能否修改属性的值 |
value | 包含该属性的数据值。默认为 undefined |
例:
var App = {
name: '其他',
}
Object.defineProperty(App,'name',{
writable: false, // 属性不可修改
configurable: false,
value: '大树云',
})
console.log(person) // 大树云
2.2 访问器属性
1. 访问器属性:这个属性不包含数据值,包含的是一对 get 和 set 方法,在读写访问器属性时,就是通过这两个方法来进行操作处理的。
2. 访问器属性包含四个特性,分别是:
属性 | 含义 |
---|---|
configurable | 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为 false |
enumerable | 表示能否通过 for-in 循环返回属性,默认为false |
get | 在读取属性时调用的函数,默认值为 undefined |
set | 在写入属性时调用的函数,默认值为 undefined |
例:
var App = {
_year: 2019, // 内部属性,只能通过对象的方法来读写
edition: 1
}
Object.defineProperty(App,'year',{
get: function(){
return this._year
},
set: function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2019;
}
}
})
App.year = 2020
console.log(App.edition) // 2 修改后的值
console.log(App.year) // 2020 修改后的值
3. 创建对象的方法
最简单粗暴:对象字面量
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
关键字做了什么把握不清。
还有两种创建对象的模式,寄生构造函数模式 和 稳妥构造函数模式,大树云项目中目前还没有用到过,这里就不做赘述。
本文代码片段参考《JavaScript 高级程序设计(第三版)》第六章
了解以下知识点或许对本文的理解更有帮助
- JavaScript new 方法做了什么
- JavaScript 基本数据类型和引用数据类型
- 栈内存与堆内存
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。