ECMA-262把对象定义为一组属性的无需集合.
这意味着对象就是一组没有特定顺序的值,对象的每一个属性和方法都由一个名称来标识,这个名称映射到一个值
所以可以把对象理解成一个散列表,内容就是一对对键值对,其值可能是数据或者函数
8.1 理解对象
创建对象的方式通常是创建Object的实例,然后再给他添加新的属性和方法
但是对象字面量更加流行
let person = {
name: 'zs',
age: 20
}
类中的属性和方法决定了他们在JS中的行为
8.1.1 属性的类型
属性分为两种:
1.数据属性
数据属性包含一个数据值的位置,有4个特征去描述行为
[[Configurable]]: 表示属性是否可以通过delete删除并且重新定义
[[Enumerable]]: 表示属性是否可以通过for-in循环返回
[[Wirtable]]: 属性值是否能被修改
[[Value]]: 表示属性实际的值
let person = {
name: 'zs',
age: 20
};
Object.defineProperty(person, "address" ,{
value: 'bj',
writable: false
})
console.log(person.address);
2.访问器属性
访问器属性不包含数据值,相反,它们包含一个getter()和setter()函数,在读取属性值时,就会调用这个函数,返回有效值,写入同理
包含4个特征属性:
[[Configurable]]: 表示属性是否可以通过delete删除并且重新定义
[[Enumerable]]: 表示属性是否可以通过for-in循环返回
[[Get]]: 获取函数
[[Set]]: 设置函数
8.1.2 定义多个属性
ECMAScript提供了Object.defineProperty() 可以通过描述符一次定义多个属性
8.1.3 读取属性的特性
通过 Object.getOwnPropertyDescriptors() 可以得到一个对象,对象的属性就是特征值
接受两个参数 属性所在对象和属性名
8.1.4 合并对象
通过 Object.assign()实现对象的合并
let dest, src, result
dest = { name: 'dest' }
src = { id: 'src' }
result = Object.assign(dest, src)
//会修改目标对象,也会返回修改后的目标对象
console.log(dest === result)
console.log(dest === src)
console.log(dest)
console.log(result)
8.1.5 对象相等及相等判定
Object.is() 和 === 很像
但是Object.is()考虑了一些 === 的边界情况
8.1.6 增强的对象语法
1.属性值简写
let name = 'zs'
let person = {
// 简写属性名只要写变量名就会被自动解析成同名属性键
name
}
2. 可计算属性
let nameKey = 'name'
let person = {
// [] 告诉JS把它当成表达式而不是字符串
[nameKey] : 'zs'
}
3.简写方法名
let nameKey = 'name'
let person = {
[nameKey] : 'zs',
sayname: function(name){
console.log(`My name is ${name}`);
},
// 简写方法名
sayname(name){
console.log(`My name is ${name}`);
}
}
8.1.7 对象解构
let person = {
name: 'zs',
age: 20,
}
// 对象结构
let { name: personName, age: PersonAge } = person
(
// 如果让变量直接使用属性名,可以简写
// 如果给事先定义过的变量赋值,要用括号包裹
({ name, age } = person)
)
(
// 可以定义默认值
({ name, job = 'engineer' } = person)
)
1.嵌套解构
可以通过嵌套解构去匹配嵌套的属性
let person = {
name: 'zs',
age: 20,
job: {
title: 'eng',
},
}
//嵌套解构
let { job: { title } } = person
但是在外层属性没有定义的情况下不能使用嵌套解构
2.部分解构
涉及多个属性的解构赋值是一个输出无关的序列化操作,如果一个解构表达式有错误,那只有前面的会成功
3.参数上下文匹配
在函数的参数列表中可以使用解构赋值
let person = {
name: 'zs',
age: 20,
job: {
title: 'eng',
},
}
function say({name,age}){
console.log(`My name is ${name},my age is ${age}`);
}
say(person);
8.2 创建对象
8.2.1 概述
ES6开始正式支持类和继承.但是仅是封装了ES5.1构造函数加原型继承的语法糖而已
8.2.2 工厂模式
用于抽象特定对象的设计过程
function createperson(name,age,job){
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayname = function(){
console.log(name);
}
return o
}
let person1 = createperson('zs',20,'stu')
工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
8.2.3 构造函数模式
构造函数是用于创建特定类型的对象的
function Person(name,age,job){
this.name = name
this.age = age
this.job = job
this.sayname = function(){
console.log(name);
}
}
let person1 = new Person('zs',20,'stu')
和工厂模式的区别:
1.没有显式地创建对象
2.属性和方法直接赋值给了this
3.没有return
要注意函数名首字母大写,这是惯例
构造函数也是函数
构造函数的问题
构造函数定义的方法会在每个实例上都创建一遍,因为每个实例都有Function实例,定义方法实际上是给每个Function实例创建属性
相当于
function Person(name,age,job){
this.name = name
this.age = age
this.job = job
this.sayname = new function(){
console.log(name);
}
// 逻辑等价
this.sayname = new Function("console.log(name)")
}
解决方法:把函数定义转移到函数外部
function Person(name,age,job){
this.name = name
this.age = age
this.job = job
this.sayname = sayname
}
function sayname(){
console.log(name)
}
8.2.4 原型模式
prototype属性是一个对象,是调用构造函数创建对象时的原型.可以直接给对象原型赋值,这样它的属性或方法可以被对象实例共享
function Person(){}
Person.prototype.name = 'zs'
Person.prototype.age = 20
Person.prototype.job = 'eng'
Person.prototype.sayname = function() {
console.log(name)
}
1.理解原型
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有
2.层级原型
通过对象访问属性时,会现在对象实例开始搜索,没找到才去原型上面找
虽然可以通过实例读取原型对象的值,但是不能通过实例重写.当实例创建同名值时,只会覆盖住原型对象上的属性,当实例的值被删除,原型的值又能被访问到了
3.原型和in操作符
in可以判断属性是否能被实例访问到(在实例上或原型上)
hasOwnProperty()可以判断实例上有无属性
如果要确定一个属性在不在原型上,可以:
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in object)
}
想要列出所有属性名,可以使用Object.getOwnPropertyNames()
同样的,Object.getOwnPropertySymbols()可以列出所有符号
4.属性枚举顺序
除了Object.getOwnPropertyNames(),Object.getOwnPropertySymbols()和Object.assign(),其他顺序都是不确定的
上三个会先以升序枚举数值键,然后根据插入顺序枚举字符串和符号
8.2.5 对象迭代
Object.values()返回对象值的数组
Object.entries() 返回键值对的数组
这两个静态方法用于把对象内容转化为序列化的,可迭代的格式
其中,非字符串的属性会被转化成字符串,符号属性会被忽略
实例只有指向原型的指针,没有指向原型构造函数的指针
原型的问题:
1.弱化了向构造函数传递初始化参数的能力
2.共享特性,导致不同的实例会拥有相同的属性副本(所以开发中通常不单独使用原型模式)
8.3 继承
ECMA-262把原型链定义为主要继承方式
基本思想:通过原型继承多个引用类型的属性和方法
实现原型链涉及以下模式
function SuperType(){
this.property = true
}
SuperType.prototype.getSuperValue = function(){
return this.property
}
function SubType(){
this.Subproperty = false
}
// 继承
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function(){
return this.property
}
原型链拓展了前面描述的原型搜索机制,对属性和方法的搜索会一直持续到原型链的末端
1.默认原型
所有引用类型都继承自Object,所以原型链还有一环Object,处于原型链的最顶端
2.原型与继承关系
1.使用instanceof操作符 原型链中如果出现过相应构造函数,返回true
2.使用isPrototypeOf() 原型链中如果出现过相应原型,返回true
3.关于方法
当子类需要增加或覆盖方法时,这些方法必须在原型赋值之后再添加到原型上
※以字面量创建原型方法会破坏之前的原型链,因为相当于重写了原型链
4.原型链的问题
1.原型中包含引用值时,引用值会在原型间共享,所以属性一般会在构造函数上定义而不是在原型上定义
2.子类型在实例化的时候不能给父类型的构造函数传参
8.3.2 盗用构造函数
盗用构造函数又叫对象伪装或经典继承
基本思路:在子类构造函数中调用父类构造函数
可以用apply()和call()方法以新调用的对象为上下文执行构造函数
function SuperType(){
this.property = true
}
function SubType(){
SuperType.call(this)
}
1.传递参数
盗用构造函数可以向父类构造函数传值
function SuperType(name){
this.name = name
}
function SubType(){
SuperType.call(this,'zs')
this.age = 20// 实例属性
}
let instance = new SubType()
console.log(instance.name);// zs
console.log(instance.age);// 20
2.盗用构造函数的问题
必须在构造函数中定义方法,因此函数不能复用
子类也不能访问父类原型上定义的方法
所以盗用构造函数不能单独使用
8.3.3 组合继承
综合了原型链和盗用构造函数,把两者的优点集合了起来
基本思路:通过原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性
function SuperType(name){
this.name = name
}
SuperType.prototype.sayAge = function (){
console.log(this.age);
}
function SubType(name,age){
// 继承属性
SuperType.call(this,name)
this.age = 20// 实例属性
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function (){
console.log(this.age);
}
8.3.4 原型式继承
ES5通过 Object.create() 方法把原型式继承的方法概念规范化了
传入两个参数:作为新对象原型的对象,给新对象定义额外属性的对象(可选),在只有一个参数时,和object()等价
let person = {
name: 'zs',
friends: ['A','B','C']
}
let d = Object.create(person)
d.name = 'ls'
d.friends.push('d')
let e = Object.create(person)
e.name = 'ww'
e.friends.push('e')
console.log(person.friends); //['A', 'B', 'C', 'd', 'e']
Object.create() 和 Object.defineProperties()的第二个参数一样,每个新增的属性都通过格子的描述符来描述
let person = {
name: 'zs',
friends: ['A','B','C']
}
let d = Object.defineProperties(person,{
name: {
value: 'ls'
}
})
console.log(d.name);// ls
原型式继承适合不需要单独创建构造函数,但是还是要在对象间共享信息的场合
8.3.5 寄生式继承
和原型式继承比较接近,创建一个实现继承的函数,以某种方式增强对象,然后返回对像
function createAnother(original){
let clone = object(original)// 调用函数创建一个新对象
clone.satHi = function(){// 以某种方式增强对象
console.log('hi');
}
return clone// 返回
}
和构造函数模式类似,会导致函数难以复用
8.3.6 寄生式组合继承
组合继承存在的效率问题:父类构造函数始终会调用两次
寄生式组合继承基本思路:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype) // 创建对象
prototype.constructor = subType // 增强对象
subType.prototype = prototype // 赋值对象
}
寄生式组合继承可以说是最佳的引用类型继承模式
8.4 类
前面讲述了如何使用ES5的特性来模拟类的行为
ES6新引入的class具有正式定义类的能力,但是背后使用的仍然是原型和构造函数的概念
8.4.1 类定义
有两种主要方式:类声明和类表达式
// 类声明
class person {}
//类表达式
const Animal = class {}
类的构成:
构造函数constructor() 实例方法 获取函数get 静态方法static
8.4.2 类构造函数
方法名constructor会告诉解释器在使用new创建类的新实例时要调用这个函数
1.实例化
使用new调用类的构造函数会进行如下操作:
1.在内存中创建一个新对象
2.在新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性
3.构造函数内部的this被赋值为这个新对象
4.执行构造函数内部的代码
5.如果构造函数返回的对象非空,则返回;否则返回创建的新对象
类实例化传入的参数会用作构造函数的参数,如果不需要参数,则类名后面的括号可以省略
类构造函数和构造函数的主要区别:调用类构造函数必须使用new操作符,否则就会作为全局的window对象作为内部对象
ECMAScript中没有正式的类这个类型.类就是一种特殊函数
8.4.3 实例,原型和类成员
1.实例成员
每次使用new调用类标识符,都会执行类构造函数.在函数内部,可以为新创建的实例this添加属性
每个实例都对应一个唯一的成员,这意味着所有成员都不会再原型上共享
2.原型方法与访问器
在类块中定义的方法称为原型方法
在类块中定义的所有内容都会在类的原型上,通过 类名.prototype获取
类方法等同于对象属性
3.静态类方法
可以用static定义静态方法,通常用于执行不特定于实例的操作
// 存在于不同的实例
constructor() {
this.locate = () => console.log('instance', this)
}
// 在类的原型
locate() {
console.log('prototype', this)
}
//在类本身
static locate() {
console.log('class', this)
}
}
let p = new Preson()
p.locate()
Person.prototype.locate()
Person.locate()
4.非函数原型和类对象
在类外部可以手动添加类的数据成员
class Person {
constructor() {
this.locate = () => console.log('instance', this)
}
}
Person.greeting = 'hi'
5.迭代器与生成器方法
类定义语法支持在原型和类本身定义生成器
class Person {
constructor() {
this.nickname = ['a','b','c']
}
*[Symbol.iterator](){
yield *this.nickname.entries()
}
}
8.4.4 继承
ES6原生支持了类继承机制,背后仍然时原型链
1.继承基础
使用extends实现单继承
class Bus extends Vehicle {}
2.super()
在派生类中,可以通过super引用它们的原型
注意点:
super只能在派生类的构造函数或静态方法中使用
不能单独引用super关键字,要么调用构造,要么调用静态
调用super()会调用父类构造函数,并且把返回的实例赋值给this
super()的行为类似与调用构造函数,要参数得手动输入
如果没有定义构造函数,则会自动调用super()并传入参数
不能在super之前使用this
如果派生类显式地定义了构造函数,那么必须使用super()或者返回一个对象
3.抽象基类
可以通过new.target实现
class Vehicle {
constructor(){
console.log(new.target);
if(new.target === Vehicle){
throw new Error('Vehicle不能被直接取代')
}
}
}
4.继承内置类型
ES6中提供了继承内置类型的顺畅通道,可以方便地扩展内置类型
class SuperArray extends Array {
shuffle(){
// 实现的代码
}
}
5.类混入
把不同类的行为集中到一个类
具体方法是定义一组"可嵌套"的函数,每个函数接受一个超类作为对象,把混入类定义为这个函数的子类,并返回这个类.在连缀调用组合函数后,会组成一个超类表达式
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo(){
console.log('foo');
}
}
let BarMixin = (Superclass) => class extends Superclass {
bar(){
console.log('bar');
}
}
let BazMixin = (Superclass) => class extends Superclass {
baz(){
console.log('baz');
}
}
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle)))
注意:许多JS框架已经抛弃混入转向了组合模式.反映了一个软件设计原则:组合胜过继承