6.1 理解对象
6.1.1 属性类型
ECMAScript 分为数据类型和访问器类型(可通过 Object.defineProperty 修改)
1.数据属性
-
configurable:是否可以用 delete 删除属性,能否修改该属性的特性或访问器属性
-
enumerable:是否能通过 for-in 循环返回该属性
-
writable:是否可写
-
value:这个属性的数据值
2.访问器属性
-
configurable
-
enumerable
-
Get:在读取属性时调用
-
Set:在写入属性时调用
6.1.2 定义多个属性
Object.defineProperties(obj, {
property: {
....
}
})
6.1.3 读取属性特征
-
getOwnPropertyDescriptor(obj, property):获取对象给定属性的描述符
6.2 创建对象
6.2.1 工厂模式
function createPerson(name, age) {
let o = new Object()
o.name = name
o.age = age
o.sayName = function() { console.log(this.name) }
return 0
}
let person = createPerson('hzb', 18)
问题:没有办法知道这个对象的类型
6.2.2 构造函数模式
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function() { console.log(this.name) }
}
let person = new Person('hzb', 18)
-
用 new 操作符创建一个对象会经历一下4个步骤
-
创建一个新对象
-
将构造函数的环境对象给新对象(将this指向这个新的对象)
-
执行构造函数中的代码(为新对象添加属性)
-
返回新对象
-
-
对象的 constructor 可以用来标识对象的类型,但是对象检测还是使用 instanceof 操作符比较靠谱
-
构造函数和普通函数一样,只是调用方式不同
-
问题:构造函数的主要问题就是每个方法都要在实例上重新创建一遍
6.2.3 原型模式
每个函数创建的时候都会创建一个 prototype 属性指向函数的原型对象。所有的原型对象都有一个 constructor 属性指向 prototype 属性所在函数的指针。(这个属性也是共享的)
当调用一个构造函数创建一个新的实例后,该实例的内部有一个 __proto__ 指向构造函数的原型对象。

-
可以通过 isPrototypeOf 确定对象之间是否存在原型关系
Person.prototype.isPrototyoeOf(person1)
-
Object.getPrototypeOf():获取实例的原型对象
Object.getPrototypeOf(person1) === Person
-
创建一个新对象
-
将构造函数的环境对象给新对象(将this指向这个新的对象)
-
执行构造函数中的代码(为新对象添加属性)
-
返回新对象
-
对象的 constructor 可以用来标识对象的类型,但是对象检测还是使用 instanceof 操作符比较靠谱
-
构造函数和普通函数一样,只是调用方式不同
-
通过 hasOwnPrototype(property) 可以检测到属性是否存在于实例之中
-
in 操作符可以通过对象访问给定属性是否存在,存在返回 true,不存在返回 false,无论属性是在实例还是在原型中
-
使用 for-in 循环的时候,返回的是所有能够通过对象访问的属性,除了原型中不可枚举的属性。(实例中不可枚举的属性也会出现)
-
如果只想取得可枚举的属性可以通过 Object.keys()
-
想得到实例中所有的属性,包括不可枚举的属性可以通过 Object.getOwnPropertyNames(),返回实例中所有属性的字符串数组
-
问题:原型中的所有属性和方法都会被共享,如果有一个属性是引用类型的,那么也会被共享
6.2.4 组合使用构造函数模式和原型模式
这个模式是最常见的模式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性
6.2.5 动态原型模式
为了解决构造函数的定义原型方法分离的问题,我们可以使用动态原型模式,它把所有信息都封装在了构造函数之中
function Person(name, age) {
this.name = name
this.age = age
if (typeof this.sayName != 'function') {
this.prototype.sayName = function() { console.log(this.name) }
}
}
-
使用 if 检查的可以是初始化之后应该存在的任何共享的属性或方法,不必用一堆 if 检查每个属性和方法
-
使用动态原型模式不能用字面量重写原型
6.2.6 寄生构造函数模式
-
寄生构造函数和工厂模式其实是一模一样的,除了调用方法不一样,寄生构造函数是使用new操作调用,工厂模式是直接调用。
-
构造函数在不返回值的情况下,默认会返回新对象实例。而通过构造函数的末尾添加一个 return 语句,可以重写调用构造函数时的返回值。
-
这个模式可以在特殊情况下为对象创建构造函数。
-
寄生模式是专门用来给js原生的构造函数定义方法,避免污染到其他原生对象。
function SpecialArray() {
let values = nwe Array()
values.push.apply(values, arguments)
values.toPipedString = function() {
return this.join('|')
}
return values
}
像上面的例子中,SpecialArray()返回了一个对象,这个values与Array“唯一”的区别就是比Array多了一个自定义的方法。。如果直接在Array中定义新的方法,会污染其它的数组对象甚至造成不必要的麻烦。
6.2.7 稳妥构造函数模式
-
稳妥对象指的是没有公共属性,而且其方法也不能使用 this 的对象。
-
稳妥对象不使用 new 操作符调用构造函数
function Person(name, age, job) {
let o = new Object()
// 这里定义私有变量和函数
o.sayName = function() {
console.log(name)
}
return o
}
-
在这种模式下,只有 sayName 可以访问到 name 属性。适合在某些安全执行环境下使用。
6.3 继承
许多 OO 语言都支持接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
ECMAScript 只支持实现继承
6.3.1 原型链
原型链的基本思想是一个引用类型继承另一个引用类型的属性和方法。
每一个构造函数都有一个原型对象,原型对象都包含着一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如一个原型对象是另一个对象的实例,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。
原型链的问题:
-
引用类型的原型属性会被所有实例共享。而通过原型链来实现继承,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就会顺理成章的变成现在的原型属性。
-
在创建子类型的实例时,没有办法在不影响所有对象实例的情况下,向超类型函数传递参数。
6.3.2 借用构造函数继承
通过 call、apply 在构造函数内部调用父函数,继承父函数的属性和方法
优点:能独享继承属性和方法
缺点:代码无法复用,无法做到属性和方法共享
6.3.3 组合继承(经典继承)
在子类型的构造函数中通过 call 或 apply 借调超类型的构造函数,实现对原型的属性和方法的继承。
并把子构造函数传进来的参数,一部分传给超类型的构造函数,一部分定义为自身的属性。
为了确保超类型构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加子类型中定义的属性。
function SuperType (name) {
this.name = name
this.colors = ['red', '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.sayAge = function() {
console.log(this.age)
}
组合继承是 JS 中最常用的继承模式
缺点:组合式继承的问题在于无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型时,一次是在子类型构造函数内部。
6.3.4 原型继承
原型继承是基于已有的对象创建新的对象,同时还不必因此创建自定义类型。为了让子函数能修改父函数的原型
var circle = {
create(raduis) {
var circle = Object.create(this)
circle.raduis = raduis
return circle
},
area () {
var raduis = this.raduis
return radius * radius * Math.PI
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
}
var circle2 = circle.create(10); // circle2.__proto__ === circle
// 可以通过 Object.getPrototypeOf(obj) 读取 obj 的原型,如
Object.getPrototypeOf(circle2) // 为 circle
// 也可以通过 Object.setPrototypeOf(obj, prototype) 为obj 设置原型对象
原型继承和经典继承的区别:
https://www.imooc.com/wenda/detail/582746
6.3.5 寄生
式
继承
寄生式继承本质上就是创建一个仅用于封装继承过程的函数,该函数内部在以某种方式来增强对象,最后再把对象返回。
function createAnother(original) {
var clone = object(original)
clone.sayHi = function() {
console.log('hi')
}
return clone
}
缺点:不能复用
6.3.6 寄生组合
式
继承
组合式继承的问题在于无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型时,一次是在子类型构造函数内部。
寄生组合继承就是为了解决这个问题的。本质上就是使用寄生式继承来继承超类型的原型,然后将结果指定给子类型(不用实例化父类,直接实例化一个父类的原型的副本,子类的原型指向父类的副本)
function inheritPrototype(subType, superType) {
var prototype = clone(superType.prototype)
prototype.constructor = subType
subType.prototype = superType
}
function SuperType (name) {
this.name = name
this.colors = ['red', 'yellow']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) // 这里调用了 SuperType
this.age = age
}
inheritPrototype(subType, superType) // 这样subType.prototype 就不会有SuperType实例中多余的属性和方法了,只需要 SuperType 原型中的属性和方法即可。
SubType.prototype.sayAge = function() {
console.log(this.age)
}