本章内容
- 理解对象:属性,内部特性,解构赋值
- 理解对象创建过程:字面量,工厂,构造函数,原型
- 理解继承:原型链,盗用构造函数,组合式,组合式寄生
- 理解类:构造函数,实例成员,原型方法,静态类方法,继承
对象
JavaScript的对象是一组由键-值组成的无序集合
对象的键都是字符串类型,值可以是任意数据类型。
其中每个键又称为对象的属性,要获取一个对象的属性,用
对象变量.属性名
创建对象2种方法
1 | // 1. new Object(),然后手动添加属性,现在不怎么用了 |
属性的类型和特性
内部特性/属性描述符:
- 用来描述属性的特征,开发者不能在JS中直接访问,用双中括号
[[]]
表示 - 属性分为数据属性和访问器属性,有不同的内部特性
数据属性:p349
包含一个保存数据值的位置。
4个特性
- [[Configurable]] 是否可delete,是否可修改特性,是否可以改成访问器属性
- [[Enumerable]] 是否可以通过for-in返回(遍历对象属性)
- [[Writable]] 是否可修改
- [[Value]] 值
使用
Object.defineProperty()
方法修改
访问器属性:p351
- 没有值,但是有一个getter函数或一个setter函数,可用于私有成员 p351
- https://www.w3school.com.cn/js/js_object_accessors.asp
- 4个特性
- [[Configurable]] 是否可delete,是否可修改特性,是否可改为数据属性
- [[Enumerable]] 是否可以通过for-in循环返回
- [[Get]] 获取函数,在读取属性时调用
- [[Set]] 设置函数,在写入属性时调用
- 使用
Object.defineProperty()
方法修改,也可以直接定义set和get
1 | // 直接定义set get 对象访问器 |
定义多个属性
Object.defineProperty(obj, prop)
一次只能定义一个属性的特性
Object.defineProperties(obj, props)
方法可以一次定义多个属性及其特性 p352
1 | var person = { |
读取属性的特性
Object.getOwnPropertyDescriptor(obj,prop)
取得指定属性的属性描述符
Object.getOwnPropertyDescriptors(obj)
ES2017新增,返回对象的说有属性及其描述符
合并对象 *
合并:把源对象所有的本地属性一起复制到目标对象上
Object.assign(dest,src)
方法,源对象复制到目标对象,返回目标对象
本质上是执行
执行浅拷贝:p356
- 可以有多个src,如果src之间有相同的属性,那么最终dest的那个属性使用最后一个复制的值
- 浅拷贝复制的是对象的引用,仍然指向同一个地址,所以如果属性值是对象,拷贝后修改,源和目的都会改变。
- 另,如果src中有getter函数会报错,p357。要复制getter用
Object.getOwnPropertyDescriptor(obj,prop)
和Object.defineProperty(obj, prop)
对象标识及相等判定 *
Object.is()
方法,ES6新增
===
无法判定的情况:
1 | // 这些是===符合预期的情况 |
Object.is()
:
1 | console.log(Object.is(true, 1)); // false |
检查多个值,递归调用
1 | function recursivelyCheckEqual(x, ...rest) { |
增强的对象语法
ES6新增语法糖(为了让编程更简洁优美添加的语法,比如for循环就是基于while的语法糖)
属性值简写
- 属性名和变量名一样
obj{name:name}
时可以省略成obj{name}
可计算属性/符号属性
可以使用变量的值作为属性
1
2
3
4
5
6
7
8
9
10
11// 以前
const a="aaa"
let obj1={}
obj1[a]="bbb"
// {aaa: 'bbb'}
// 可计算属性
let obj2={
[a]:"ccc"
}
// {aaa: 'ccc'}
方法名简写
- 以前定义对象中的方法:
fun : function(val){...}
- 现在简写:
fun(val){...}
对象解构
p362
1 | // 使用对象解构 |
嵌套结构p364
创建对象
创建对象的方式 *
ES6正式支持类和继承
创建对象方式:
- 对象字面量
- 工厂模式
- 构造函数模式
- 原型模式
1 | person={ |
工厂模式
一个工厂能提供一个创建对象的公共接口,我们可以在其中指定我们希望被创建的工厂对象的类型,也就是工厂函数就是这个接口,调用函数,返回一个实例,不需要new
1 | function createPerson(name, age, job) { |
优点:可以创建多个类似对象
缺点:没有解决对象标识问题(不知道新创建的对象是什么类型)
构造函数模式
1 | function Person(name, age, job) { |
与工厂模式的区别:
- 没有显式的new Object;属性方法赋给了this,没有return,创建对象时要new
- 另,构造函数函数名要首字母大写比较规范
- 另另,构造函数写成函数声明或是函数表达式都可以
优点:可以识别创建对象的类型(访问实例的Constructor属性)
缺点:构造函数定义的方法会在每个实例都创建一遍
- 一种解决方法是把函数定义在对象外,对象里引用,但是这样就污染了全局作用域,不好,解决方法见原型模式
原型模式 *
每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。使用原型对象的好处是,在它上面 定义的属性和方法可以被对象实例共享。
可以用函数声明或函数表达式
1 | function Person() { |
注意:
- 通过prototype定义的属性存在在原型上,被所有实例共享
- 在构造函数内定义的属性(例中instance),只有在创建实例之后,才会为每个实例单独分配一个
- 上面两者的区别在属性值为对象时很明显:第一种只要在一个实例里修改了这个对象,原型里和其它实例里都会变,第二种就是独立的
优点:解决了实例属性和方法共享的问题
缺点:需要在外部依次定义prototype的属性,比较繁琐(不能一次性定义,因为prototype会被设置成一个通过字面量创建的新对象,它的constructor就不指向Person了,见p386)
1 | // 用字面量批量定义属性会有问题!!!不要用!!! |
01 理解原型 *
构造函数、原型对象和实例
- 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
1)构造函数创建之后,自动创建一个原型对象属性prototype,指向原型对象
- 原型对象自动获得一个constructor属性,指向与之关联的构造函数
- 在自定义构造函数时,原型对象默认只会获得constructor属性, 其他的所有方法都继承自Object。
1 | function Person() {} |
2)每次调用构造函数创建一个新实例(person1)
这个实例的内部**[[Prototype]]指针就会被赋值为构造函数的 原型对象**(Person.prototype)。
脚本中没有访问这个**[[Prototype]]特性的标准方式, 但Firefox、Safari和Chrome会在每个对象上暴露
__proto__
属性,通 过这个属性可以访问对象的原型**。在其他实现中,这个特性完全被 隐藏了。
1 | let person1 = new Person(), |
检查、修改构造函数、原型对象和实例的关系
A instanceof B
运算符- 检测构造函数的
prototype
属性是否出现在某个实例对象的原型链上
- 检测构造函数的
isPrototypeOf()
方法- 检测原型对象和实例的关系
getPrototypeOf()
方法- 返回实例对象对应的[[Prototype]]值
setPrototypeOf()
方法- 修改实例对象对应的[[Prototype]]值,不推荐使用
1 | console.log(person1 instanceof Person); // true |
02 原型层级
在通过对象访问属性:按照属性名称开始搜索。
- 先搜索对象实例本身。如果发现该属性名称,则返回对应值。
- 如果在实例对象没有找到该属性,则沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
属性遮蔽:
- 在对象实例添加属性,则会遮蔽原型对象的同名属性(不修改原型,只屏蔽访问)
- 可以通过delete操作符删除实例的属性,回复对原型的访问
1 | person1.name="b" |
判断一个对象是否包含某个属性 *
in 操作符
通过对象可以访问指定的属性时,返回true
1
2‘name’ in person1; // true
‘toString’ in person1; // true
hasOwnProperty()
属性存在于实例,返回true,存在于原型或继承的false
1
2person1.hasOwnProperty(‘name’); //true
person1.hasOwnProperty(‘toString’); //false
03 原型和in操作符 *
in操作符两种使用方式:
单独:可以通过对象访问指定的属性时,返回true
for in循环:返回可以通过对象访问且可以被枚举 的属性
- 实例属性和原型属性都可以
- 可枚举就表示属性的[[enumberable]]特性为true
获得对象上的所有属性名称(字符串):
- Object.keys() 返回对象自身所有可枚举属性
- sObject.getOwnPropertyNames() 返回对象自身所有属性
- Object.getOwnPropertySymbols() ES6新增,返回对象自身所有符号属性(见Symbol)
1 | let keys = Object.keys(Person.prototype); |
04 枚举顺序
for in 和 Object.keys() 顺序不确定 见p384
对象迭代
ES2017新增 迭代对象的静态方法
Object.keys()
Object.values() :返回对象属性值的数组
Object.entries() :返回对象属性键值对的数组
注:非字符串属性会转换为字符串,符号属性会忽略,值为对象时执行浅拷贝
04 原型的问题 *
p390
原型上的属性在所有实例之间共享
- 可以 通过在实例上添加同名属性来简单地遮蔽原型上的属性
- 但是,对于包含引用值的属性,会有问题:
- 一个实例修改了这个属性的值,其实是修改了原型上的属性值,会反映到其他所有实例上
- 不同实例之间应该有属于自己的属性副本,所以开发时不会单独使用原型模式
继承
通过原型链实现继承
原型链 *
构造函数、原型 和实例的关系:
- 构造函数都有一个原型对象,
Person.prototype
- 原型有一个属性指回 构造函数,
Person.prototype.constructor===Person
- 实例有一个内部指针指向原型。
person.__proto__===Person.prototype
原型链就是:一个构造函数的原型是是另一个类型的实例,这样整个原型就有一个内部指针([[Prototype]])指向另一个原型,即子类的原型指向父类的原型
ES5的继承写法:将子类的prototype对象重新定义为一个父类的实例(默认所有引用类型都是继承自Object)
1 | function SuperType() { |
A instanceof B
运算符检测构造函数的
prototype
属性是否出现在某个实例对象的原型链上1
2
3console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
isPrototypeOf()
方法检测原型对象和实例的关系
1
2
3console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
03 增加方法
如果子类需要覆盖父类的方法或增加父类没有的方法,那么需要在子类的原型上定义(注意:子类的原型需要先改写成父类的实例再定义方法)
1 | function SuperType() { |
04 原型链的问题 *
原型中包含的引用值会在所有实例之间共享,所以,在使用原型实现继承时
- 子类原型是父类的实例。
- 父类的实例属性变成了子类的原型属性。
- 实例属性本来是每个实例单独拥有的,但是变成原型属性之后就会变成共享状态
- 属性值为对象时:由于原型属性在所有子类间共享,所以一个实例改变这个值,所有属性也都改变
1 | function SuperType() { |
盗用构造函数 *
为了解决原型包含引用值导致的继承问题
盗用构造函数/对象伪装/经典继承
在子类的构造函数中调用父类的构造函数
使用call()或apply()进行调用
1 | function SuperType(name) { |
优点:解决了引用值的问题,且可以在子类构造函数向父类构造函数传参
问题:在构造函数中定义方法,会在每个实例中创建一个新的方法,因此函数不能重用,在子类也是创建一个新方法,而不是访问父类原型上定义的方法
组合继承 **
组合继承/伪经典继承:
- 通过原型链继承原型上的属性和方法:重用方法
- 通过盗用构造函数继承实例属性:每个实例都有单独的属性
1 | function SuperType(name){ |
原型式继承
适用情况:不需要单独创建构造函数,但仍然需要在对象间共享信息的场合,也就是在一个对象的基础上创建新对象
使用Object.create() 方法,传入参数:作为原型的对象,新增的属性(通过描述符表示)
1 | let person = { |
优点:不在意类型和构造函数
缺点:和原型模式一样,引用值会共享
寄生式继承
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
1 | function createAnother(original){ |
优点:不在意类型和构造函数
缺点:与盗用构造函数模式类似,函数难以重用
寄生式组合继承 *
3.3的组合继承存在问题:
子类的原型上会多出一组实例属性,因为:p403
声明父类构造函数时,定义了实例属性
将子类原型定义成父类实例时,创建了一组实例属性
寄生式组合继承就是解决上面的问题
1 | function inheritPrototype(subType, superType) { |
优点:是引用类型继承的最佳模式
类
ES6新加入的语法糖
类定义
建议类名首字母大写
1 | // 类声明 |
和函数的区别:
函数声明可以提升,类定义不能
函数受函数作用域限制,类受块作用域限制
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法(方法名前加static只能在类中调用)
类构造函数
1 | class Person { |
实例化的过程见p408
类构造函数和构造函数的区别:
- 类构造函数一定要new调用!不然报错
- 普通构造函数不new的话就会默认变成全局对象
实例 原型 类成员
实例成员
- 通过类构造函数的this添加,或直接给创建好的实例添加,每个实例都有唯一成员对象,不会共享
原型方法
- 类块中定义的方法,共享,等于属性,跟对象一样可以用字符串,符号,或计算的值为键,setter,getter也一样
静态类方法
- 用static关键字作前缀
- 属于类,this引用类自身,不属于类实例,只能被类调用
非函数类型和类成员
- 类块里面不能直接定义类成员,但是类块外面可以用
类名.成员名
定义
继承
01 基础
extends关键字继承任何有[[Construct]]和原型的对象(可以继承类或构造函数,向后兼容)
1 | // 类声明 |
02 构造函数 *
通过super调用父类构造函数和静态方法
1 | class Vehicle { |
03 抽象基类
可以被其他类继承,但本身不会被实例化。虚基类?
new.target保存通过new关键字调用的类 或函数。通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化
通过在抽象基类构造函数中进行检查,可以要求派生类必须 定义某个方法。
1 | // 抽象基类 |
05 类混入
多类继承:p425
- 在一个表达式中连缀多个混入元素,这个 表达式最终会解析为一个可以被继承的类。
- 例:想要P组合ABC:B基础A,C继承B,然后P继承C
- 已抛弃,用组合模式(把方法提取到独立的类和辅助对象中, 然后把它们组合起来,但不使用继承)
总结
理解原型
构造函数、原型对象和实例
- 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
1)构造函数创建之后,自动创建一个原型对象属性prototype,指向原型对象
- 原型对象自动获得一个constructor属性,指向与之关联的构造函数
- 在自定义构造函数时,原型对象默认只会获得constructor属性, 其他的所有方法都继承自Object。
2)每次调用构造函数创建一个新实例
- 这个实例的内部**[[Prototype]]指针就会被赋值为构造函数的 原型对象**
- 脚本中没有访问这个**[[Prototype]]特性的标准方式, 但Firefox、Safari和Chrome会在每个对象上暴露
__proto__
属性,通 过这个属性可以访问对象的原型**
在通过对象访问属性:按照属性名称开始搜索。
- 先搜索对象实例,发现属性则返回
- 未发现,则沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
原型的问题:原型上的属性在所有实例之间共享
- 可以 通过在实例上添加同名属性来简单地遮蔽原型上的属性
- 但是,对于包含引用值的属性,会有问题:
- 一个实例修改了这个属性的值,其实是修改了原型上的属性值,会反映到其他所有实例上
- 不同实例之间应该有属于自己的属性副本,所以开发时不会单独使用原型模式
理解原型链
构造函数、原型 和实例的关系:
- 构造函数都有一个原型对象,
Person.prototype
- 原型有一个属性指回 构造函数,
Person.prototype.constructor===Person
- 实例有一个内部指针指向原型。
person.__proto__===Person.prototype
原型链就是:一个构造函数的原型是是另一个类型的实例,这样整个原型就有一个内部指针([[Prototype]])指向另一个原型,即子类的原型指向父类的原型
原型链的问题:原型中包含的引用值会在所有实例之间共享,所以,在使用原型实现继承时
- 子类原型是父类的实例。
- 父类的实例属性变成了子类的原型属性。
- 实例属性本来是每个实例单独拥有的,但是变成原型属性之后就会变成共享状态
- 属性值为对象时:由于原型属性在所有子类间共享,所以一个实例改变这个值,所有属性也都改变
继承的方法
- 原型链继承:包含引用值的属性会共享
- 盗用构造函数继承:无法重用方法
- 原型链+盗用构造函数组合继承:1+2 使用最多
- 原型式继承:没有构造函数,缺点类似1
- 寄生式继承:没有构造函数,缺点类似2
- 寄生式组合继承:2+4,最有效
- 类继承,好
原型链
优点:共享原型方法
缺点:原型对象是引用值时,共享引用,改一个实例就全改
1 | // 1. 原型链 |
盗用构造函数
优点:实例有各自的属性值
缺点:方法只能定义在构造函数里,难以重用
1 | // 2. 盗用构造函数 |
组合继承
优点:实例有各自的属性值,不会干扰,方法也可以重用
缺点:子类的原型上会多出一组实例属性,因为:p403
声明父类构造函数时,定义了实例属性
将子类原型定义成父类实例时,创建了一组实例属性
1 | // 3. 组合继承 |
寄生式组合
优点:解决了普通组合式的问题
1 | // 6. 寄生式组合继承 |
类
1 | class Super{ |
区别:prototype
和__proto__
构造函数方法
每一个构造函数有一个prototype指针,指向构造函数声明时自动创建的原型对象
只要是对象就会有一个[[Prototype]]内部属性,这个属性在chrome浏览器中可以被__proto__
属性暴露出来
- new出来的实例对象的
__proto__
指向构造函数的原型对象 - 构造函数的
__proto__
指向Function的原型对象,因为本质上它是一个函数 - 任何原型对象的
__proto__
指向Object的原型对象,因为本质上它是一个对象
1 | function Person(){} |
构造函数继承
原型链继承时,本质上是把子类的原型指向父类的实例,因为父类的实例的__proto__
指针能指向父类原型
子类的原型上的
__proto__
指向父类原型子类实例上的
__proto__
指向子类原型,本质上是父类的实例
1 | function Adult(){} |
类继承
使用class声明类时prototype
和__proto__
的指向跟构造函数时完全一样
- new出来的实例对象的
__proto__
指向构造函数的原型对象 - 构造函数的
__proto__
指向Function的原型对象,因为本质上它是一个函数 - 类的原型对象的
__proto__
指向Object的原型对象,因为本质上它是一个对象
1 | class Person{} |
- 子类的原型上的
__proto__
指向父类原型 - 子类实例上的
__proto__
指向子类原型
1 | class Adult extends Person {} |
注意,*类的成员方法就是定义在类的原型上的