《JavaScript高级程序设计(第三版)》学习笔记
相关内容:
关键词:对象的prototype属性;实例属性;创建对象的方式:原型模式、构造函数模式、寄生模式
本章节学习路线:
什么是对象?对象的属性?
创建对象的模式:工厂模式、构造模式、 原型模式、动态原型模式 、寄生构造函数模式、稳妥构造函数模式
原型模式深入理解:
- 原型模式的优点与问题 - 共享
- 问题的解决方法:构造函数与原型模式的使用 - 构造函数存实例属性,原型对象存共享属性
1. 关于对象的理解
// 创建对象
var person = {
name: 'kenny',
age: 29,
sayName: function () {
alert(this.name) // Object中可直接创建对象
}
}
1.1 属性类型
1.1.1 数据属性
数据属性:包含一个数据值的位置。该位置可进行读取和写入
属性值 | 定义 | 默认值 |
---|---|---|
[[Configurable]] | 能否修改属性的特性,一旦修改为false,则再也无法恢复 | true |
[[Enumerable]] | 能否通过for-in循环返回属性 | true |
[[Writable]] | 能否修改属性的值 | true |
[[Value]] | 该属性的数据值 | undefined |
用Object.defineProperty()
可以对属性进行修改:(但是慎重使用)
var person = {}
Object.defineProperty(person, 'name', {
writable: false,
value: 'Teller'
})
1.1.2 访问器属性
访问器属性:不包含数据值,包含getter和setter
属性值 | 定义 | 默认值 |
---|---|---|
[[Configurable]] | 能否修改属性的特性,一旦修改为false,则再也无法恢复 | true |
[[Enumerable]] | 能否通过for-in循环返回属性 | True |
[[Get]] | 读取属性时调用 | undefined |
[[Set]] | 写入属性时调用 | undefined |
1.2 定义多个属性 - Object.defineProperties()
var book = {}
Object.defineProperties(book, {
_year: {
writable: true,
value: 2004
},
edition: {
writable: true,
value: 1
},
year: {
get: function() {
return this._year
}
}
})
1.3 读取属性的特性 - Object.getOwnPropertyDescriptior()
任何DOM和BOM对象均可使用该方法,以获取对象属性
var descriptor = Object.getOwnPropertyDescriptior(book, '_year')
alert(descriptor.value) // 2004
alert(descriptor.configurable) // false
2. 创建对象的方式
关键词:工厂模式、构造函数、原型模式、组合使用、动态原型、稳妥构造函数、寄生构造函数
2.1 工厂模式
- 工厂模式:用函数封装,构造整个对象,并在末尾return这个对象
- 存在的问题:无法使用instanceof操作符判断对象类型(因该方法并非构造函数模式) - 用构造函数解决该问题
function createPerson (name, age, job) {
// 1. 创建新对象
var o = new Object()
// 2. 为对象添加属性
o.name = name
o.age = age
o.job = job
o.sayName = function () {
alert(this.name)
}
// 3. 需要在末尾return这个对象
return o
}
// 调用函数构造实例 - 无需使用new操作符
var person1 = createPerson('Greg', 27, 'Doctor')
// 存在的问题 - 无法使用instanceof判断person1是createPerson类型的对象
alert(person1 instanceof createPerson) // false
alert(person1 instanceof Object) // true
2.2 构造函数模式
-
构造函数:将属性和方法直接赋值给this对象,无需return
-
与工厂模式的区别:
- 没有显式的创造对象
- 直接将属性和方法给了this对象
- 没有return语句
-
解决了工厂模式的问题:用构造函数创建的对象,可以通过instanceof,获取到对象的类型
-
构造函数的缺点:功能效果完全相同的属性无法共享,重复占据内存空间 - 用原型模式解决该问题
// 构造函数Person(一般为首字母大写)
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function() {
alert(this.name)
}
}
// 调用构造函数的方式:
// 1. 用new方法,使用构造函数,创建person对象
var person1 = new Person('Greg', 29, 'Doctor')
alert(person1 instanceof Person) // true - 可使用instanceof获得对象类型
// 2. 直接作为普通函数使用,则为给window创建创建对象 - 此时Person()中的this指向window
Person('Greg', 29, 'Doctor')
window.sayName() // 'Greg'
// 存在的问题:构造函数创建的属性为实例属性,其中功能效果完全相同的属性无法共享,重复占据内存空间
alert(person1.sayName == person2.sayName) // false
/*
* 例如:通过Person构造的两个对象的sayName()是不等价的,但两者执行的功能是一致的
* 普通解决方法:将共享属性从构造函数中移出,作为全局属性
* 产生的新的问题:如果一个对象需要有多个function,就要全局声明多个function,违反了封装性
* 最佳解决方案:原型模式
*/
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName() { // 全局function - 缺乏封装性
alert(this.name)
}
2.3 原型模式 - prototype
2.3.1 原型模式的创建方法与基本概念
- 原型模式:每一个创建的函数、Object都有一个prototype属性
- prototype属性是一个指针,指向一个对象,包含由特定类型的所有实例共享的属性核方法
- 优点:让所有对象实例均可共享其所包含的属性和方法,而无需在构造函数中定义对象实例的信息
- 注意:实例中创建的同名属性,会覆盖掉原型中的属性
function Person() {} // 构造函数Person
// 在Person的原型中添加属性,而非在构造函数中添加
Person.prototype.name = 'Nicholas'
Person.prototype.name = 29
Person.prototype.sayName = function () { alert(this.name) }
// 创建对象 - 使用Person构造函数创建Person对象,该对象可以直接使用Person中的原型属性
var person1 = new Person()
person1.sayName() // 'Nicholas'
var person2 = new Person()
person2.sayName() // 'Nicholas'
alert(person1.sayName == person2.sayName) // true - 创建不同对象时,均指向同一个原型属性(同一个指针)
- 另一种创建原型的方法:对象字面量法
// 对象字面量法
Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'Doctor',
sayName: function() {
alert(this.name)
}
}
对象字面量法存在问题:prototype对象的constructor属性,不再指向Person
原因:通过上述方法,完全重写了默认的prototype对象,所以constructor属性变成了新对象的constructor属性,指向Object构造函数,而非Person函数
var person1 = new Person()
alert(person1 instanceof Object) // true
alert(person1 instanceof Person) // true
alert(person1.constructor == Person ) // false - person1对象的构造函数因为重写prototype对象,所以直接指向了Object
alert(person1.constructor == Object) // true
// 处理上述问题的方法:在重写的对象中也把constructor进行赋值返回
Person.prototype = {
constructor: Person, // 在重写的原型对象中,把Person设置为constructor的值
name: 'Nicholas',
age: 29,
job: 'Doctor',
sayName: function() {
alert(this.name)
}
}
// 但上述方法会导致constructor变成可枚举的值,实际上constructor是默认不可枚举的,所以需要用defineProperty方法进行设置
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false, // 重置为false
value: Person
})
2.3.2 原型对象的理解 - 共享属性
-
图解上述代码样例: 图解说明如下:
- Person Prototype - 原型对象:包含构造函数constructor和属性值:name、age、sayName
- Person 构造函数:原型Prototype指向整个Person原型对象;原型对象中的构造函数指回了Person构造函数
- 利用Person构造函数创建的Person1和Person2对象:包含一个[[Prototype]]属性,该属性指向的就是Person原型对象 所以,Person1和Person2对象,均指向的是同一个原型对象中的sayName属性
A. 原型对象解析(根据上图总结):
1. 创建一个新函数function,就会为该函数创建一个prototype属性,该属性指向函数的原型对象
2. 所有的原型对象都会自动获得一个constructor属性,该属性是一个指向prototype属性所在函数的指针(注意:指针)
3. 通过构造函数创建一个对象后,该对象内部将包含一个`[[prototype]]`指针,指向构造函数的原型对象
B. 可以通过isPrototype()
和Object.getPrototypeOf()
方法确定对象是否指向构造函数(因为在常规实现中无法访问):
// 1. 用isPrototype()方法确定对象和构造函数之间关于prototype的关系
Person.prototype.isPrototypeOf(person1) // true
// 2. 用es5提出的Object.getPrototypeOf()来返回[[Prototype]]的值
Object.getPrototypeOf(person1).name // 'Nicholas'
Object.getPrototypeOf(person1) == Person.prototype // true
C. 无法通过对象实例重写原型中的值,只能通过在实例中创建同名的该属性,以屏蔽原型中的属性
// 1. 创建构造函数Person
function Person() {}
// 2. 创建Person的原型对象
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Doctor'
Person.prototype.sayName = function() { alert(this.name) }
// 3. 创建Person的对象person1
var person1 = new Person()
// 4. 在实例(constructor)中创建name属性值
person1.name = 'Greg'
// 5. 实例中的name属性覆盖掉prototype中的name属性
alert(person1.name) // 'Greg'
// 6. 通过delete删除实例中的属性值
delete person1.name
// 7. 由于删除了实例中的属性,所以调用时候会展示原型中的属性值
alert(person1.name) // 'Nicholas'
2.3.3 判断属性在原型还是在实例中的方法
A. 判断属性是否来自于实例:hasOwnProperty()
- 来自于实例中则返回true
var person1 = new Person()
// 1. name属性来自于原型中,故返回false
alert(person1.hasOwnProperty('name')) // false
// 2. 创建实例中的属性,则检测后返回true
person1.name = 'Greg'
alert(person1.hasOwnProperty('name')) // true
B. 判断属性是否来自于实例或对象:hasOwnProperty()
、单独使用in操作符
in
操作符:可单独使用或在for-in
循环中使用,单独使用时可以判断属性是否存在于实例或原型中
// 用in方法确认name属性是否存在于实例或原型中
var person1 = new Person()
alert('name' in person1) // true --- name属性存在于实例或原型中
Object.hasOwnProperty()
为Object方法,可用于判断属性是否存在于实例中
// 用Object.hasOwnProperty()方法确认属性是否存在于实例中
var person1 = new Person()
alert(person1.hasOwnProperty('name')) // false --- name属性不存在于实例中
- 另外:可以将上述两个方法进行自定义封装,从而实现判断属性是否存在于原型中
// 利用Object.hasOwnProperty()和in操作符,封装一个自定义的方法,判断属性是否存在于原型中
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object)
}
alert(hasPrototypeProperty(person, 'name')) // true --- name属性存在于原型中
C. 通过for-in方法可访问Enumerated为true的属性值,但会屏蔽Enumerated为false的值
// 通过Object.keys()方法,返回包含所有可枚举属性的字符串数组
var keys = Object.keys(Person.prototype)
alert(keys) // 'name, age, job, sayName'
// 通过Object.getOwnPropertyNames()方法,返回所有实例属性(无论是否可枚举)
var keys2 = Object.getOwnPropertyNames(Person.prototype)
alert(keys2) // 'constructor, name, age, job, sayName' --- constructor不可枚举,但可以通过该方法获取
2.3.4 原型的动态性 - 动态修改原型
A. 动态修改原型:对原型对象所做的任何修改可立即在实例中反映出来
// 1. 直接创建对象friend
var friend = new Person()
// 2. 然后为Person添加原型对象属性sayHi
Person.prototype.sayHi = function() {
alert('Hi')
}
// 3. 对象friend可以使用新添加的属性值
friend.sayHi() //'Hi'
B. 注意:如果使用对象字面量法重写原型对象,会切断构造函数和最初原型对象的关系
原因:实例中的指针是指向原型,而非构造函数
// 1. 直接创建对象friend
var friend = new Person()
// 2. 然后为Person重写原型对象,包含属性sayHi
Person.prototype = {
constructor: Person, // 在重写的原型对象中,把Person设置为constructor的值
name: 'Nicholas',
age: 29,
job: 'Doctor',
sayName: function() {
alert(this.name)
}
}
// 3. 对象friend不能使用通过重写原型对象而新添加的属性值,
// 原因:Person本身指向了一个全新的prototype对象,而非friend指向的原有的原型对象
friend.sayHi() //error
2.3.5 原生对象也具备原型模式
- 原生对象:例如:Array、String等
- 所有的原生的引用类型,都是上述模式创建的,具备原型模式。例如:
Array.prototype
中可以找到sort()
方法;String.prototype
中可以找到substring()
方法 - 可以随时给原生对象添加新的原型方法,但是不推荐使用
2.3.6 成也原型,败也原型 - 浅析原型对象的问题:共享属性
- prototype最优秀的特性,就是具备共享能力,可以把一些公有的方法、属性作为原型属性,让所有实例共享使用
- 但,prototype的缺点也来自于这种共享的特性:如果prototype中存在引用类型,则会使不同实例的该属性值相互干扰
- 解决方案:共享属性放在原型中,非共享属性(需避免干扰的)放在构造函数中 ------------ 即为:构造函数模式和原型模式的组合使用(详见2.4节)
// 创建构造函数Person
function Person() {}
// 创建原型对象,其中包含引用类型friends(Array)
Person.prototype = {
constructor: Person, // 在重写的原型对象中,把Person设置为constructor的值
name: 'Nicholas',
age: 29,
job: 'Doctor',
friends: ['Sheldon', 'Raj']
sayName: function() {
alert(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
// person1对象中,为引用对象属性friends赋值
person1.friends.push('Penny')
// 由于原型对象的共享性,所以所有通过Person()构造函数创建的对象,都具备相同的原型对象属性,原型对象中的引用对象会指向同一个值
// 问题:两个实例的共享属性互相干扰
alert(person1.friends) // 'Sheldon, Raj, Penny'
alert(person1.friends) // 'Sheldon, Raj, Penny'
alert(person1.fiends === person2.friends) // true
2.4 构造函数模式和原型模式的组合使用
- 用以解决上述原型模式共享属性带来的互相干扰的问题,并最大限度节省内存空间
- 充分结合利用构造函数模式与原型模式的特点:
- 构造函数模式:定义实例属性
- 原型模式:定义方法和共享的属性
// 创建构造函数Person - 非共享属性,尤其是引用类型,作为实例属性
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Sheldon', 'Raj']
}
// 创建Person的原型对象 - 将共享的属性,例如function,作为原型属性
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name)
}
}
var person1 = new Person('Nicholas', 29, 'Doctor')
var person2 = new Person('Cindy', 28, 'Developer')
person1.friends.push('Van')
// 实例属性互不相同,但共享属性指向同一指针
alert(person1.friends === person2.friends) // false
alert(person1.sayName === person2.sayName) // true
2.5 动态原型模式
- 动态原型模式:通过检查某个应该存在的方法是否有效,来决定是否需初始化原型
- 优点:可以更进一步优化内存空间
- 注意:使用动态原型模式时,不能使用对象字面量重写原型。因为已经创建了实例的情况下重写原型,会切换现有实例和新原型之间的关系
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Sheldon', 'Raj']
// 仅在sayName不存在的情况下,才将其添加到原型中
// 添加方法不可使用对象字面量方法,
if (typeof this.sayName != 'function') {
Person.prototype.sayName = function() {
alert(this.sayName)
}
}
}
2.6 寄生构造函数模式
- 寄生构造函数模式:创建一个函数,该函数的作用仅为封装创建对象的代码,再返回新创建的对象
- 用途:用于将原生对象(例如:Array)添加自定义属性方法而不修改原生对象的构造函数(对原生对象的增强)
- 特点:
- 创建方法类似工厂模式,但工厂模式的函数不是构造函数,本模式的包装函数可以称为构造函数
- 可以使用new操作符
- 构造函数中的返回对象可以重写
- 构造函数返回的对象,与在构造函数外部创建的对象相同
- 无法通过instanceof来确定对象的类型
- 不推荐使用该模式!
// 样例1:
// 以类似工厂模式的方法,创建寄生构造函数模式
function Person(name, age, job) {
var o = new Object() // 创建对象
o.name = name
o.age = age
o.job = job
o.sayName = function () {
alert(this.name)
}
return o // 返回对象
// 如果不设置返回值,则默认会返回新对象实例;故可以在寄生模式中重写返回值
}
// 可以使用new操作符创建,因为上述方法可以被认为是构造函数
var friend = new Person('Nicholas', 29, 'doctor')
friend.sayName() // 'Nicholas'
// 样例2:重写并为Array方法添加属性
// 不能修改Array构造函数,但是可以使用寄生模式为对象添加构造函数
function SpecialArray () {
var values = new Array() // 创建数组对象
values.push.apply(values, arguments) // 添加值
values.toPipedString = function () { // 添加方法
return this.join('|')
}
return values // 返回数组
}
var colors = new SpecialArray('red', 'blue', 'green')
alert(colors.toPipedString()) // 'red|blue|green'
2.7 稳妥构造函数模式
- 稳妥对象:没有公共属性,方法不引用this的对象,适合在安全环境中使用(安全模式下禁止this)
- 与寄生模式创建方法相似,不同点如下:
- 新创建对象的实例方法不引用this
- 不使用new操作符调用构造函数
- 注意:稳妥模式下不能使用对象字面量重写原型,原因:对象字面量方法会切断与最初构造函数之间的关系
function Person(name, age, job) {
// 创建对象
var o = new Object()
o.name = name
o.age = age
o.job = job
// 不使用this对象
o.sayName = function () {
alert(name)
}
// 返回对象
return o
}
var friend = Person('Nicholas', 29, 'doctor')
// 除了使用Person中的sayName,不能通过其他方法获取name,故适合安全环境
friend.sayName() // 'Nicholas'
3. 小结
-
对象的基本属性:数据属性、访问器属性
-
创建对象的方式: | 创建对象方式 | 简要说明 | | ---------------- | ------------------------------------------------------------ | | 工场模式 | 用函数封装,构造整个对象,并在末尾return这个对象
缺点:无法使用instanceof操作符判断对象类型(因该方法并非构造函数模式)
解决方案:用构造函数解决该问题 | | 原型模式 | 直接使用prototype,利用prototype的共享特性;
缺点:引用对象存于prototype时,会导致创建的不同实例的该原型属性值互相干扰
解决方案:构造+原型组合模式 | | 动态原型 | 通过检查某个应该存在的方法是否有效,来决定是否需初始化原型,以更进一步优化存储空间 | | 构造+原型 | 构造部分存自身实例属性
原型部分存共享的属性(例如function) | | 寄生构造函数函数 | 可用于给原生对象(例如Array)增强添加一些自定义方法或属性而不破坏原有对象构造函数
缺点:不可用instanceof确定对象类型 | | 稳妥构造函数 | 新创建对象的实例方法不引用this,在安全模式下稳妥运行
不使用new操作符调用构造函数 | -
原型的问题:引用对象放在prototype中,会因为共享导致一处改变、处处改变
-
寄生的本质:增强 - 在原有基础上添加自定义新属性
笔记目录:
JavaScript学习(1) - JavaScript历史回顾
JavaScript学习(3)- 聊聊原型链- 2. 对象与原型
JavaScript学习(3)- 聊聊原型链- 3. 原型链与继承
Github: