高程第六章摘要

摘要自《JavaScript 高级程序设计》(第3版),如有侵权请联系面向对象

创建面向对象:

var person = new Object()

person.name = 'magua'

person.age = 29

person.sayName = function() {

​ alert(this.name)

}

也可以写成这样

var person = {

​ name: 'magua',

​ age: 29,

​ sayName: function() {

​ alert(this.name)

​ }

}

属性类型

在 ECMAScript 中有两种属性:数据属性和访问器属性

数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性

Configurable(结构的,可配置的) : 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。

Enumerable(可枚举的,可点数的):表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认为 true

Writable (可写下的):表示能否修改属性的值。

Value (数据值) :包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值得时候,把新值保存在这个位置。这个特性的默认值为 undefined

对于像前面例子中那样直接在对象上定义的属性它们的 Configurable/Enumerabla/Writable 特性都被设置为 true,而 Value 特性被设置为指定的值,例如

​ var person = {

​ name: 'magua'

​ }

这里创建了一个名为 name 的属性,为它指定 的值是 'magua' 。也就是说, Value 特性将被设置为 'magua',而对这个值得任何修改都将反映在这个位置

要修改属性的默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法。这个方法接受三个参数:属性所在的对象,属性的名字和一个描述符对象。其中描述符对象的属性必须是数据属性。设置其中一个或多个值,可以修改对应的特性值。例如:

​ var person = {}

​ Object.defineProperty(person, 'name', {

​ writable: false,

​ value: 'newMagua'

​ })

​ alert(person.name); // 'newMagua'

​ person.name = 'gua'

​ alert(person.name); // 'newMagua'

这里使用了 Object.defineProperty 方法修改了前面例子的 name 属性,并将它的 Writable 属性设置为不可改,如果尝试为它指定新值,则在非严格模式下,赋值操作将被忽略;在严格模式下,赋值操作将会导致抛出错误。

类似的规则也适用于不可配置的属性。例如:

​ var person = {}

​ Object.defineProperty(person, 'name', {

​ configurable: false,

​ value: 'ma'

​ })

​ alert(person.name) // 'ma'

​ delete person.name

​ alert(person.name) // 'ma'

一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用 Object.defineProperty() 方法修改除 writable 之外的特性,都会导致错误。

在调用Object.defineProperty() 方法时,如果不指定,configurable/enumerable/writeable 特性的默认值都是 false

访问器属性

访问器属性不包含数据值;它们包含一对 getter 和 setter 函数(这两个函数不是必需的)在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性

Configurable :表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为 true

Enumerable(可枚举的,可点数的):表示能否通过 for-in 循环返回属性。

Get : 在读取属性时调用的函数,默认为 undefined

Set : 在写入属性时调用的函数,默认为 undefined

访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义,例如

var book = {

​ year: 2000,

​ edition: 1

}

Object.defineProperty(book, 'year', {

​ get: function() {

​ return this.year

​ }

​ set: function(newValue) {

​ if(newValue > 2000) {

​ this.year = newValue

​ this.edition += newValue - 2000

​ }

​ }

})

boo.year = 2006

log(book.edition) // 6

以上设置了访问器属性 year 包含一个 getter 函数 和 一个 setter 函数。 getter 函数返回 year 的值, setter 函数通过计算来确定正确的版本。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化

不一定要同时指定 getter 和 setter。只指定一个的情况下在严格模式下会抛出错误,非严格模式下,只有 getter 时,写入属性会被忽略; 只有 setter 时,读取属性会返回 undefined

定义多个属性

利用 Object.defineProperties() 方法可以一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应,例如

var book = {}

Object.defineProperties(book, {

​ year: {

​ value: 2000

​ },

​ edition: {

​ value: 1

​ },

​ _year: {

​ get: function() {

​ return this._year

​ }

​ set: function(newValue) {

​ if(newValue > 2000) {

​ this._year = newValue

​ this.edition += newValue - 2000

​ }

​ }

​ }

})

以上代码在 book 对象上定义了两个数据属性(year 和 edition)和一个访问器属性(year)。最终的对象和上一节中定义的对象相同。唯一的区别是这里的属性都是在同一时间创建的

读取属性的特性

使用 Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。这个方法接受两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable/enumerabla/get/set; 如果是数据属性,这个对象的属性有 configurable/enumerable/writable/value

创建对象

工厂模式:

function createPerson(name, age) {

​ var o = new Object()

​ o.name = name

​ o.age = age

​ o.sayName = function() {

​ log(this.name)

​ }

​ return o

}

var person1 = createPerson('ma', 19)

var person2 = createPerson('gua', 19)

*缺点 没有解决对象识别的问题

构造函数模式:

function Person(name, age,) {

​ this.name = name

​ this.age = age

​ this.sayName = function() {

​ alert(this.name)

​ }

}

var person1 = new Person('ma', 19)

var person2 = new Person('gua', 19)

工厂模式于构造函数模式的不同之处:

没有显示地创建对象;

直接将属性和方法赋给了 this 对象;

没有 return 语句;

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤

一、创建一个新对象

二、将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)

三、执行构造函数中的代码(为这个新对象添加属性)

四、返回新对象

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;这正是构造函数模式胜过工厂模式的地方。在这个例子中,person 1 和 person2 所以同时是 Object 的实例,是因为所有对象均继承自 Object。

构造函数于其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数模式毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数。

当不使用new 操作符调用 Person() 时,会发生如下结果 :

Person('ma', 19) // 添加到了 window 中

window.sayName() // 'ma'

属性和方法都被添加给 window对象了

var o = new Object();

Person.call(o, 'ma', 19)

o.sayName() // 'ma'

可以使用 call() / apply() 在某个特殊对象的作用域中调用 Person() 函数。这里是在对象 o 的作用域中调用的,因此调用 o 就拥有了所有属性和 sayName() 方法

*缺点 :主要问题是每个方法会在每个实例上重新创建一遍。在前面的例子中, person1 和 person2 都有一个 sayName() 的方法,但那两个方法不是同一个 function实例。以这种方式创建的函数,会导致不同的作用域链和标识符解析,但创建 function 新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,可以通过把函数定义转移到构造函数外部来解决这个问题,但是那样实际上是将函数设置成等于全局的 sayName 函数,如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性

原型模式

我们创建的每个函数都有一个原型属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 原型 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

function person() {

}

person.prototype.name = 'magua'

person.prototype.age = 19

person.prototype.sayName = function() {

​ log(this.name)

}

在此,我们将 sayName() 方法和所有属性直接添加到了 person 的 prototype 属性中,构造函数变成空函数。与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。

更简单的原型语法(对象字面量创建原型方法)

function Person() {}

Person.prototype = {

​ // constructor: Person *如果 constructor 的值很重要,可以这样特意设置回适当的值。但是这种方式重设 constructor 属性会导致它的可枚举特性被设置为 true。默认情况下,原生的 constructor 属性是不可枚举的。

​ name: 'magua',

​ age: '19'

​ sayName: function() {

​ log(this.name)

​ }

}

在上面的代码中,我们将 Person.prototype 设置为等于一个对象字面量形式创建的新对象,最终结果相同,但有一个例外:constructor 属性不再指向 Person 了。我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数)

理解原型对象

无论什么时候,只要创建了一个函数,就会根据一组特定的规则为该函数创建一个 原型 属性,这个属性指向函数的原型对象。在默认情况下,所有的原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针

创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针( [[Prototype]] ),指向构造函数的原型对象。这个链接存在于实例和构造函数的原型对象之间。

isPrototypeOf() :

用于测试实例的是否包含指向构造函数所在的原型对象的指针

Object.getPrototypeOf() :

返回 指针[[Prototype]] 的值 *使用它可以方便的取得一个对象的原型,而这在利用原型实现继承的情况下是非常重要的。

如果在实例中添加一个属性,而该属性与实例原型的一个属性同名,那该属性将会屏蔽原型中的那个属性,使用 delete 操作符则可以删除实例属性,恢复其指向原型的链接。

hasOwnProperty() :

用于检测一个属性是存在于实例中还是原型中, 只有当给定属性存在于对象实例中时,返回 true。

in操作符 :

当通过对象能够访问给定属性时返回 true,无论该属性是存在于实例中还是原型中。

for-in 循环:

使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举(enumerable)的属性。根据规定,所有开发人员定义的属性都是可枚举的。将 enumerable 属性标记为 false 的属性为不可枚举。

Object.keys()

接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,用于取得对象上所有可枚举的'实例'属性。

Object.getOwnPropertyNames()

接收一个对象作为参数,返回所有的实例属性,无论是否可枚举。

原型对象的缺点:

function Person() {}

Person.prototype = {

​ friends: ['A', 'B'],

}

var p1 = new Person()

var p2 = new Person()

p1.friends.push('C')

log(p2.friends) // 'A', 'B', 'C'

这里由于 friens 数组是存在于 Person.prototype 而非 p1 中,所以修改了之后也会在 p2 反映出来。

组合使用构造函数模式和原型模式(动态原型模式)

function Person(name, age, job) {

​ this.name = name

​ this.age = age

}

Person.prototype = {

​ constructor: Person,

​ sayName: function() {

​ log(this.name)

​ }

}

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。此时创建了实例之后,再去修改其中的引用类型,比如 friends 并不会相互影响,因为它们分别引用不同的数组。

寄生式构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,适用于前面几种模式都适合的情况下,

function Person(name, age, job) {

​ var o = new Object()

​ o.name = name

​ o.age = age

​ o.sayName = function() {

​ log(this.name)

​ }

​ return o

}

var function = new Person('magua', 19)

这里,除了使用 new 操作符 并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

关于寄生式构造函数,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式,不使用这种模式。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境,或者在防止数据被其他应用程序改动时使用。

稳妥模式新创建对象的实例方法时,不引用 this;不使用 new 操作符调用构造函数。

function Person(name, age) {

​ var o = new Object

​ o.sayName = function() {

​ log(name)

​ }

​ return o

}

以这种模式创建的对象中,除了使用 sayName 方法,没有其他办法访问 name 的值。

var friend = Person('magua', 19)

friend.sayName() // 'magua'

变量 friend 中保存的是一个稳妥对象,而除了调用 sayName() 方法外,没有别的方式可以访问其数据成员。及时有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的 这种安全性,使得它非常适合在某些安全执行环境。

继承

实际继承的主要方法 原型链

其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

简单回顾构造函数、原型和实例的关系:

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,则此时原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针 ;假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例于原型的链条,这就是所谓原型链的基本概念。

实现原型链有一种基本模式,其代码大致如下

function SuperType() {

​ this.property = true

}

SuperType.prototype.getSuperValue = function() {

​ return this.property

}

function SubType() {

​ this.subproperty = false

}

// 继承了 SuperType

SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function() {

​ return this.subproperty

}

var instance = new SubType()

log(instance.getSuperValue() ) // true

以上代码定义两个类型 SuperType 和 SubType 。

每个类型分别有一个属性和一个方法。它们的主要区别是 SubType 继承了 SubperType , 而继承是通过创建 SuperType 的实例,并将实例赋给SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.protoType 中了。在确立了继承之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 Super.prototype 的属性和方法的基础之上又添加了一个新方法。

在上面的代码中,我们没有使用 SubType 默认提供的原型,而是给它换了一个新原型;这个新原型就是 SuperType 的实例。于是,新原型不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了 SuperType 的原型。最终结果是 instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。 getSuperValue() 方法仍然还在 SuperType.prototype 中,但 property 则位于 SubType.prototype 中。因为 property 是一个实例属性,而 getSuperValue() 则是一个原型方法。

通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制。当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用 instance.getSuperValue() 会经历三个搜索步骤:

一、搜索实例

二、搜索 SubType.prototype

三、搜索 SuperType.prototype

在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。

instanceof

log( instance instanceof Object ) // true

只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。

isPrototypeOf()

log( Object.prototype.isPrototypeOf(instance)) // true

谨慎地定义方法

子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。在给原型添加方法的代码一定要放在替换原型的语句之后。并且在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为对象字面量创建原型方法会重写原型链。

原型链的问题

在通过原型来实现继承时,原型实际上会变为另一个类型的实例。于是,原先的实例属性也就顺理成章地变为了现在的原型属性,包含引用类型值会被所有实例共享。第二个问题:在创建子类型的实例时,没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数,因此,实践中很少会单独使用原型链。

借用构造函数

基本思想:在子类型构造函数的内部调用超类型构造函数。

实际上就是通过改变 要继承的父类 的 this 为子类的 this 实现继承的,如下

function SuperType() {

​ this.colors = ['red', 'blue', 'green']

}

function SubType() {

​ SuperType.call(this)

}

var one = new SubType()

var two = new SubType()

one.color.push('black')

log('one', one) // one 'red', 'blue', 'green', 'black'

log('two', two) // two 'red', 'blue', 'green'

这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码,这样 SubType 的每个实例就都会具有自己的 colors 属性的副本

借用构造函数的优势:可以再子类型构造函数中向超类型构造函数传递参数

function SuperType(name) {

​ this.name = name

}

function SubType() {

​ SuperType.call(this, 'magua') // 继承了父类的同时,还传递了参数

​ this.age = 29 // 实例属性

}

缺点

方法都在构造函数中定义,函数复用无从谈起。

组合继承

基本思想: 是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承

function SuperType(name) {

​ this.name = name

​ this.n = [1, 2, 3]

}

SuperType.prototype.sayName = function() {

​ log(this.name)

}

function SubType(name, age) {

​ // 继承属性

​ SuperType.call(this, name)

​ this.age = age

}

// 继承方法

SubType.prototype = new SuperType()

SubType.prototype.constructor = SubType;

组合继承是 JS 中最常用的继承模式, instanceof 和 isPrototypeOf() 也能够用于识别基于组合继承创建的对象

寄生组合式继承

基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

function inheritPrototype(subType) {

​ var prototype = object(superType.prototype) // 创建对象

​ prototype.constructor = subType //增强对象

​ subType.protoype = prototype

}

这个例子中,

第一步、创建超类型原型的一个副本

第二部、为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性

第三步、将新创建的对象赋值给子类型的原型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值