blog-md
blog-md copied to clipboard
JS 面向对象的程序设计
面向对象的程序设计
理解对象
属性类型
- 有两种:数据属性和访问器属性。
数据属性:
-
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性改为访问器属性。 -
[[Enumerable]]
:表示能否通过for-in
循环枚举属性。 -
[[Writable]]
:表示能否修改属性的值。 -
[[Value]]
:表示属性数据值。默认为undefined
。
- 需要注意的是,前三个属性,直接在对象上定义属性时,默认都是
true
,但是如果用Object.defineProperty()
方法,不写明默认为false
. -
Object.defineProperty()
接受三个参数,对象名,属性,一个 json 对象(描述符对象)。其中描述符独享的属性必须是configurable、enumerable、writable、value
。
//Object.defineProperty()默认configurable 等属性均为 false
var person = {}
Object.defineProperty(person,'name',{
value:'Nicholas'
})
console.log(person.name) //Nicholas
person.name = 'James'
console.log(person.name) //Nicholas
- 可以多次修改同一个属性通过
Object.defineProperty()
方法,但Configurable
属性一旦设置为 false,就再也无法改回,而且除了Writable
属性可以继续修改,其他的都没法改动了。
访问器属性:
-
[[Configurable]]
同上。 -
[[Enumerable]]
同上。 -
[[Get]]
读取属性时调用的函数,默认为undefined
. -
[[Set]]
写入属性时调用的函数,默认为undefined
.
- 访问器属性不包含数据值,取而代之的是一对儿 getter 和 setter 函数(非必须)。
-
_year
前面的下划线约定俗成的表示只能通过对象方法访问的属性。 - getter和 setter 不需要同时指定,代表只能读不能写或者只写不能读,只指定 getter 函数时,写入属性会被忽略,严格模式下报错。
//Object.defineProperty()方法为访问器属性添加 getter/setter函数
var book = {
_year: 2004,
edition: 1
}
Object.defineProperty(book, 'year', {
get: function () {
return this._year
},
set: function (value) {
if (value > 2004) {
this._year = value
this.edition += value - 2004
}
}
})
book.year = 1999
console.log(book.edition,book.year) //get方法了_year 的值给了 book.year
book.year = 2005
console.log(book.edition)
定义多个属性
-
Object.defineProperties()
接受两个参数,第一个是要添加和修改属性的对象,第二个是一个 json 对象,包含一一对应的属性和值。 - 和之前的单个方法基本类似的。
创建对象
工厂模式
- 工厂模式抽象了创建具体对象的过程,用函数封装以特定接口创建对象的细节,在函数内部创建对象赋予属性和方法,最后返回这个对象。
- 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题。
构造函数模式
- 构造函数模式和工厂模式极其类似,只不过函数名大写开头,没有显示的创建对象,把属性和方法直接付给了
this
对象,而且没有return
语句。 - 按照惯例,构造函数始终以大写字母开头,非构造函数应该以小写字母开头。
- 实际上构造函数的调用经历了下面四个过程:
- 创建一个新对象
- 将构造函数的作用域赋给新对象,从而
this
就指向了新对象 - 执行构造函数的代码,为新对象添加新属性
- 返回新对象
function Person(name, age) {
this.name = name
this.age = age
this.sayHi = function() {
console.log('hi')
}
}
var person1 = new Person('james', 29)
-
person1
实例有一个constructor
构造函数属性,指向的是构造函数Person
。 - 创建自定义的构造函数可以将他的实例标识为一种特定的类型,这正是胜过工厂模式的地方。
构造函数当做函数
Person('Nicholas', 29)
console.log(window.sayHi()) //hi
Person.call(o, 'James', 33) //通过 call 和 apply 等进行特殊作用域调用
- 构造函数与其他函数的唯一区别,就在于调用的方式不同。任何函数,只要通过
new
操作符进行调用,那么他就可以作为构造函数,不通过new
来调用,和其他函数无异。 - 直接调用的话,不通过
new
操作符,会被添加到window
对象。
构造函数的问题
- 每个方法都要在实例上重新定义一遍,即使函数是完全一样的。如果把方法变成全局函数声明,全局作用域有点名不符实,而且太多的全局方法完全没有了封装性可言。
原型模式
- 每一个函数都有一个
prototype
属性,是一个指针,指向一个对象,这个对象包含了有特定类型所有实例共享的属性和方法。 - 使用原型对象的好处是可以让对象实例共享它所包含的属性和方法。
理解原型对象
- 默认情况下,所有原型对象都会获得一个
constructor
(构造函数)属性,这个属性包含一个指向prototype
属性所在函数的指针。 - 当调用构造函数创建一个新实例时,实例内部讲包含一个指针
[[prototype]]
,指向构造函数的原型对象。这个连接存在于实例与构造函数的原型对象之间,并非实例与构造函数之间。 - 搜索对象的属性时,先搜索实例属性再原型对象。实例属性如果同名,会覆盖掉原型属性。
- 使用
delete
操作符可以完全删除实例属性。 - 使用
hasOwnProperty()
方法可以检测一个属性是存在于实例中还是原型中。只有给定属性存在于对象实例中,才会返回 true. -
in
操作符,单独使用或者for-in
循环,只要能访问到给定属性就返回 true,无论存在于实例还是原型之中。 - 所以,只要
in
返回 true 而hasOwnProperty()
返回 false,就说明是原型属性,这个可以封装成函数来实现。
// /delete 操作符可以完全删除实例属性,从而访问同名原型属性
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()
var person2 = new Person()
person1.name = "James"
console.log(person1.name) //James 来自实例
console.log(person1.hasOwnProperty('name')) //true 来自对象实例的属性
console.log('name' in person1) //true in 操作符访问到了即返回true
delete person1.name
console.log(person1.name) //Nicholas 来自原型
console.log(person2.hasOwnProperty('name')) //false 来自对象原型的属性
console.log('name' in person2) //true
var keys = Object.keys(Person.prototype)
console.log(keys) //Object.keys()返回所有可枚举的实例属性
var keys1 = Object.getOwnPropertyNames(Person.prototype)
console.log(keys1) //和上面不同的是 Object.getOwnPropertyNames()返回了所有实例属性,不管可不可以枚举
原型的动态性
- 注意的是,原型中查找值是一次搜索,所以先创建实例,后修改原型对象也是会反应出来,因为实例与原型对象的连接是一个指针,并非副本。
- 随时为原型添加属性和方法,并不会有问题,但如果重写整个原型对象,等于把原型对象的指针指向了另一个原型对象,但之前创建的实例的指针指向的还是最初的原型对象,这样就会出问题。所以先重写再构造。
原型对象的问题
- 原型的所有属性都会被实例共享,对函数复用来说再合适不过,对基本属性值也还说得过去,毕竟可以在实例上添加实例属性进行覆盖,但对引用类型属性来说就是灾难。
- 例如一个数组属性,任意的实例或者原型属性修改数组,其他的实例同样会得到变化,因为他们指向的都是原型对象中存在的那个数组。所以原型模式也很少单独使用。
组合使用构造函数模式和原型模式
- 既可以通过构造函数定义实例属性,支持传参,同时原型模式还可以对共享的方法和属性进行复用,最大限度节省了内存。这是用来定义引用类型的一种默认模式,使用最广泛,认可度最高。
//组合构造函数和原型混成的方式,是使用最广,认可最高的自定义类型的方法,可称为定义引用类型的默认方式
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Nicholas', 'James']
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name)
}
}
var name1 = new Person('haha', 29, 'software engineer')
var name2 = new Person('wowo', 33, 'software engineer')
name1.friends.push('huhu')
console.log(name1.friends) //['Nicholas', 'James', 'huhu']
console.log(name2.friends) //['Nicholas', 'James']
console.log(name1.sayName === name2.sayName) //true
动态原型模式
- 上面的混成模式中,构造函数和原型是分开的,其实可以在构造函数中对原型进行封装,通过检测原型属性(初次调用构造函数才会执行)进行动态原型初始化。
- 不可以使用对象字面量重写原型,否则会切断现有实例和新原型之间的联系。
继承
- 大部分的 OO 语言支持两种继承方式,接口继承和实现继承。接口继承只继承方法签名,实现继承则继承实际的方法。但 JS 函数没有签名,所以只支持实现继承。主要是依靠原型链来实现。
原型链
- 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个指向原型对象的内部指针。如果让原型对象等于另一个类型的实例,此时原型对象包含着一个指向另一个对象的指针,层层递进,构成了实例与原型的链条。
//原型链继承,引用类型值的原型属性会被所有实例共享
function SuperType() {
this.colors = ['green', 'blue', 'yellow']
}
SuperType.prototype.getColors = function () {
return this.colors
}
function SubType() {
}
SubType.prototype = new SuperType()
var ins1 = new SubType()
var ins2 = new SubType()
ins1.colors.push('black')
console.log(ins1.colors) //['green', 'blue', 'yellow', 'black']
console.log(ins2.colors) //['green', 'blue', 'yellow', 'black'] 引用类型的原型属性被共享
- 属性搜索会经历三个搜索步骤,第一个是搜索实例,第二个搜索
SubType.rototype
,第三个搜索Super.prototype
,找不到属性的情况下,会向下一层一层寻找。 - 所有的引用类型都是默认继承了 object,这也是所有自定义烈性都会继承
toString()和 valueOf()
. - **原型链的问题:**引用类型值的原型属性会被所有实例共享;无法向超类型的构造函数中传递函数(在不影响所有实例对象的情况下)。
借用构造函数
- 有时候也叫伪造对象或经典继承,就是在子类构造函数中调用超类型构造函数。优势是可以传参。但却无法复用函数等。
//构造函数继承
function SuperType(name) {
this.name = name
}
function SubType() {
SuperType.call(this, 'Nicholas') //构造函数可以传参,却无法继承原型方法,无法函数复用
this.age = 29
}
var ins1 = new SubType()
console.log(ins1.name) //Nicholas
console.log(ins1.age) //29
组合继承
- 借用构造函数和原型链组合,原型链实现对原型属性方法继承,通过构造函数来实现对实例属性的继承。
- 这也是最常用的继承模式,
instanceOf和 isPrototypeOf()
也可以识别相应对象。
//组合继承,最常用的继承模式
function SuperType(name) {
this.name = name
this.colors = ['green', 'blue', 'yellow']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
console.log(this.age)
}
var ins1 = new SubType('Nicholas', 33)
ins1.colors.push('black')
console.log(ins1.colors) //['green', 'blue', 'yellow', 'black']
ins1.sayName() //Nicholas
ins1.sayAge() //33
var ins2 = new SubType('haha', 29)
console.log(ins2.colors) //['green', 'blue', 'yellow']
ins2.sayName() //haha
ins2.sayAge() //29
原型式继承
- 要求必须有一个对象可以作为另一个对象的基础,在没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。
- 别忘了引用类型值的属性会被所有实例共享,和之前的原型模式是一样的。
function object(o) {
function F() {}
F.prototype = o
return new F()
} //对传进来的 o 进行一次浅复制
object()和 Object.create()是相似的
Object.create(obj, {
value: 'xxx'
}) //传两个参数 后一个为 value 值的对象
寄生式继承
- 与寄生构造函数和工厂模式相似,创建一个仅用于封装继承过程的函数,并在内部添加方法增强对象,最后返回这个对象。
- 寄生式继承为对象添加函数,无法做到函数复用而降低效率,这一点与构造函数模式类似。
//寄生继承, 主要考虑对象而不是自定义类型和构造函数
function createAnother(original) {
var clone = Object.create(original)
clone.sayHi = function () {
console.log('Hi')
}
return clone
}
var person = {
name: 'Nicholas',
age: 29
}
var anotherPerson = createAnother(person)
anotherPerson.sayHi() //hi
寄生组合式继承
- 组合继承十分常用,但也有缺点:子类调用两次父类型,第二次在实例上重写了属性来覆盖原型中的属性。
- 所谓寄生式继承,就是通过借用构造函数来继承属性,通过原型链的混成模式继承方法。不必为了指定子类型的原型而调用超类型的构造函数,所需要的无非是超类型的原型的一个副本而已。
- 本质上,就是通过寄生式继承来继承超类型的原型,再将结果指定给子类型的原型。
// 组合寄生式继承
function inherit(subType, superType) {
var prototype = Object.create(superType)
prototype.constructor = subType
subType.prototype = prototype
} //