理解对象:
属性分两种:数据属性和访问器属性;
数据属性:
数据属性有四个特性描述他们的行为:
| key | 功能 | 默认值 |
|---|---|---|
| confingurable | 属性是否可以通过delete删除 | true |
| enumberable | 属性是否可以通过for-in循环遍历 | true |
| writable | 属性值是否可以被修改 | true |
| value | 属性实际的值 | undefined |
如果需要修改属性默认特性,就需要通过object.defineProperty方法,这个方法接受三个参数, 分别是:对象本身,属性的名称,一个描述对象;
访问器属性
访问器属性也有四个特性描述他们:
| key | 功能 | 默认值 |
|---|---|---|
| configurable | 属性是否可以通过delete删除 | true |
| enumberable | 属性是否可以通过for-in循环遍历 | true |
| get | 读取函数 | undefined |
| set | 设置函数 | undefined |
定义多个属性
如果需要定义多个属性那么久用Object.definedPropiries,
接受两个参数:第一个是要添加或者修改的对象本身,第二个参数是 描述符的对象;
读取属性的特性
使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符;
接受两个参数:第一个是对象本身,另一个是要获取描述符的属性名称;
返回值:一个对象
如果是访问器属性返回的对象包含configurable,enumerable,get,set属性
如果是数据属性返回的对象包含configurable,enumerable,value,witerable;
ECMAScript 2017新增了一个Object.getOwnPrototypeDescriptors()静态方法,可以一次返回多个属性描述信息;
合并对象
ECNAScript6 专门提供了一个合并对象Object.assign()方法来合并对象; 这个方法接受一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举和自有属性复制到目标对象;
强调:对于每一个符合条件的属性,这个方法会使用源对象上的[[get]]取得属性的值,然后使用目标对象上的[[set]]设置属性的值;
举个例子:
// 特殊的例子
var dest = {
_a: 1,
set a(val) {
console.log('设置a的值:' + val);
this._a = val
}
}
var src = {
get a() {
console.log("获取a的值:");
return 'foo'
}
}
Object.assign(dest, src)
console.log(dest);
这个例子会依次打印属性Id的值:
var dest = {
set id(x) {
console.log(x);
}
}
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'thrid' })
// 最后打印内容是
// first
// second
// thrid
Object.assign()合并对象的特点:
- 实际上对每个源对象执行的都是浅拷贝;
- 如果多个源对象都有相同的属性,则使用最后一个复制的值;
- 不能再两个对象间转移获取函数和设置函数;
如果合并对象的时候出错了,Object.assign()并不会回滚操作,可能只会完成部分复制的方法
看下面这个例子:
// 回滚测试代码
var dest = {}
var src = {
a: 'foo',
get b() {
throw new Error("出错了")
},
c: 'bar'
}
try {
Object.assign(dest, src)
} catch (err) {
console.log(err);
}
console.log(dest); // {a: "foo"}
对象标识及相等判断is
ECMAScrpt6规范新增了Objece.is(),这个方法与===很想,比===更加精确;
例如:
// 用三等号,下满三个都是true
console.log(Object.is(+0, -0)) // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(0, -0)); // false
// 之前判断NaN,只能用isNaN方法判断
console.log(Object.is(NaN, NaN)); // true
增强的对象语法
- 1,属性值简写(key名字和值的变量名一样的话就可以省略)
- 2,可计算属性 (可用中括号动态改变对象的属性名)
- 3,简写方法名 (es6的方法简写)
对象解构
这个没有什么好说的,注意两点就行了:
1,使用解构赋值的时候可以起一个别名
比如:
let {name: personName, age: personAge} = person;
2,赋值的时候可以给以默认值
例如:
let {name: persionName = '默认名字', age: persionAge} = person;
重点来了:
解构在内部使用函数ToObject()把原数据结构转化为对象,这意味这在对象解构的上下文中,原始值会被当做对象,也意味着null和undefined不能被解构,会抛出错误;
看看下面的例子:
let { length } = "foobar";
console.log(length); // 6
// 4的构造函数赋值给了c
let { constructor: c } = 4
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
解构赋值还可以用在嵌套解构,部分解构,参数上下文匹配中;
创建对象
强调:ES6虽然引入了类和继承,但是本质还是对ES5的封装,只是一个语法糖而已;
创建一个对象下面几种模式:
- 工厂模式;
- 构造函数模式;
- 原型模式;
工厂模式:
function createPersion(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
}
return o;
}
var p1 = createPersion('lsj',18,'web前端');
var p2 = createPersion('zm',19,'平面设计师');
这种工厂模式虽然可以解决创建多个类似对象的问题,但是没有解决对象标识的问题(即新创建的对象是什么类型)
构造函数模式
// 编码规范,只有是构造函数就需要首字母大写;
function Persion(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log("我的名字是" + this.name);
}
}
var p4 = new Persion('lsj',18,'web前端');
var p5 = new Persion('zm',19,'平面设计师');
p4.sayName(); // 我的名字是lsj
p5.sayName(); // 我的名字是zm
这里有个经典的面试题:
创建一个对象new内部都做了什么事情?
- 1,在内存中创建了一个新的对象;
- 2,这个新对象内部[[prototype]]指向了构造函数prototype属性;
- 3,构造函数内部的this被赋值为这个新的对象;
- 4,执行构造函数内部的代码(给新对象添加属性);
- 5,如果构造函数返回的是个非空对象,则返回这个对象,否则返回刚创建的那个对象(也就是this);
在前面我们说过工厂函创建出来的对象不知道类型,那么构造函数可以解决上面问题,p4和p5分别保存着Persion的不同实力,这两个对象都有一个constructor属性指向了Persion;这个constructor就是标识对象类型的;
构造函数与普通函数的区别:
构造函数与普通函数唯一的区别就是调用方式不用。除此之外,构造函数也是函数。任何函数只要使用了new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数;
构造函数创建出来的对象有什么问题呢?
创建出来的方法每一个对象都保存了一份占用空间,所以就出现了后面的原型模式;
理解原型:
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。然后因构造函数而异,可能会给原型对象添加其他属性和方法;
在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object,每次调用构造函数创建一个新的实例,这个实例内部[[prototype]]指针就会被赋值为构造函数原型对象,脚本中没有访问这个[[prototype]]特性的标准方式,但是浏览器会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。
关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
下面看一些例子来理解:
// 声明之后构造函数就有了一个与之关联的原型对象;
function Persion() {}
console.log(typeof Persion.prototype); // object
console.log(Persion.prototype);
// 构造函数有一个prototype属性,指向其原型对象,而这个原型对象也有一个constructor属性,指向这个构造函数;
// 换句话书就是循环引用
console.log(Persion.prototype.constructor === Persion); // true
// 正常的原型链都会终止与Object的原型对象 Objecrt的原型指向的是null
console.log(Persion.prototype.__proto__ === Object.prototype); // true
console.log(Persion.prototype.__proto__.constructor === Object); // true
console.log(Persion.prototype.__proto__.__proto__); // null
// 1,实例对象通过__proto__链接到原型对象,他实际上指向隐藏属性[[prototype]]
// 2,构造函数通过prototype属性链接到原型对象;
// 3,实例与构造函数没有直接联系,与原型对象有直接的联系;
let persion1 = new Persion();
console.log(persion1.__proto__ === Persion.prototype);
// 同一个构造函数创建两个实例,共享一个原型对象(节省空间)
let persion2 = new Persion();
console.log(persion1.__proto__ === persion2.__proto__) // true
// instanceof 检查实例的原型中是否包含指定构造函数的原型
console.log(persion1 instanceof Persion) // true
console.log(persion1 instanceof Object) // true
console.log(Persion.prototype instanceof Object) //true
虽然不是所有实现都对外暴露了[[prototype]],但可以使用isPrototypeOf()方法判断两个对象之间的关系;
用法如下:
console.log(Persion.prototype.isPrototypeOf(persion2)) // true
console.log(Persion.prototype.isPrototypeOf(persion1)) // true
ECMAScript的object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[prototype]]的值;在继承的时候很有用,后面介绍;
用法如下:
console.log(Object.getPrototypeOf(persion2))
setPrototypeOf()方法,设置一个对象的原型,但是性能不好!可以通过Object.create()方法来代替;
虽然可以通过实例访问原型上的属性,但是不能修改原型上的属性,如果再实例上添加一个与原型同名的属性,会遮住原型的上的属性;这是因为访问一个对象的属性时,先从对像自己属性上找,找不到才回去原型链上去找,一层一层向上寻找;
hasOwnPrototype()方法用于确定属性实在实例上是否有该属性,有返回true,否则返回false;
in操作符:可以通过对象访问属性时候返回true,无论这个属性在对象上还是在原型上;
可以根据上面两个api实现一个hasPrototypeProtype方法,判断竖向是否在原型上:
具体代码如下
function hasPrototypePrototype(obj, name) {
return (name in obj) && !obj.hasOwnPrototype(name)
}
for in 循环注意事项:
只要能够被访问就可以被枚举出来,既包含实例属性,也包含原型属性;
如果只需要遍历对象属性,那么可以使用Object.key()方法,这个方法接受一个对象作为参数,返回包含该对象所欲可枚举属性名称的字符串数组;
下面举个例子看一下:
function Persion() { }
Persion.prototype.name = "lsj"
let persion1 = new Persion();
persion1.ag = 18
for (let key in persion1) {
console.log(key) // name age
}
let keys = Object.keys(persion1)
console.log(keys) // [age]
如果想要获取实例上的所有属性无论是否可枚举(不包含原型上的属性),可以使用Object.getOwnPropertyNames(),如果以Symbol作为key的话,需要使用它的兄弟方法遍历Object.getOwnPropertySymbols(),用法一样的;
属性枚举的顺序:
- for in
- Object.keys()
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.assign()
for in 循环和Object.keys()的枚举顺序是不确定的,取决于js引擎,可能因浏览器而已;
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键;
// 看下面的例子:
let key1 = Symbol("k1")
let key2 = Symbol("k2")
let o = {
1: 1,
first: "first",
[key1]: 'key1',
second: "second",
0: 0
}
o[key2] = 'key2'
o[3] = 3
o.thrid = 'thrid'
o[2] = 2
console.log(Object.getOwnPropertyNames(o)) // ["0", "1", "2", "3", "first", "second", "thrid"]
console.log(Object.getOwnPropertySymbols(o)) // [Symbol(k1), Symbol(k2)]
对象迭代
ESMAScript2017新增了两个静态方法,Objedt.values()和Object.entries(),接受一个对象作为参数,前者返回的是值的数组,后者返回的是键值对的数组;
值得注意的是:
- 如果值是引用类型,这两个方法拿到的都是引用,就是所谓浅复制;
- 对于键是Symbol类型,两个方法都会忽略;
重写原型
还有一种方式使我们可以重新定义Prototype指向一个新的对象;
如下:
// 定义一个构造函数
function Persion () {}
// 将构造函数的原型重新指向一个对象
Persion.prototype = {
name:'lsj',
age:18,
sayName() {
console.log("我的名字是" + this.name)
}
}
let p1 = new Persion()
p1.sayName()
但是这样写有一个问题:重写之后Persion.Prototype的consturctor属性就不知道指向Persion了;
console.log(Persion.prototype.constructor === Persion) // false
所以就不能通过constructor属性来识别类型了,也需要使用instanceof操作符来判断了;
当然我们可以修正该指针,如下代码:
// 将构造函数的原型重新指向一个对象
Persion.prototype = {
constructor:Persion, // 指针修正
name:'lsj',
age:18,
sayName() {
console.log("我的名字是" + this.name)
}
}
但是这种修正的方式和原生的有些区别(原生中constructor是不可以枚举的):
再次修改:
// 定义一个构造函数
function Persion() { }
// 将构造函数的原型重新指向一个对象
Persion.prototype = {
name: 'lsj',
age: 18,
sayName() {
console.log("我的名字是" + this.name)
}
}
Object.defineProperty(Persion.prototype, "constuctor", {
enumerable: false,
value: Persion
})
原型的动态性
表现在创建一个对象实例之后,对原型所做的修改在实例上也会反应出来;
比如:
// 定义一个构造函数
function Persion() { }
//创建实例
let p1 = new Persion()
// 修改原型
Persion.prototype.sayHello = function() {
console.log("hello")
}
// 调用原型方法
p1.sayHello()
之所以这样是因为实例和原型之间就是简单的指针关系,而不是保存副本,所以能在原型中找到sayHello方法;
需要注意一点就是创建实例之后,然后重写原型,那么之前重建实例不会动态改变了,因为指针变掉了;
一般情况:原型上定义一些公用的方法,属性最好通过传参的方式传入;
继承
原型继承
ECMS-262把原型链定义为ECMASCript的主要继承方式;基本思路就是通过原型继承多个引用类型的属性和方法;
==默认原型:==
原型链的最顶层就是Object,这就是为什么自定义的类型能够继承包括toString()、valueOf()等在内的所有默认方法的原因;
==继承和原型之间的关系==:
原型和实例的关系可以通过两种方式来确认:第一个是instanceof,第二个是isPrototypeOf(),具体用法可以参考P241页;
==关于方法:==
子类有时候需要覆盖父类的方法,或者重写父类的方法,为此,这些方法必须原型赋值之后再添加到原型上;
==原型链的问题:==
原型中包含引用类型的时候,会在实例中共享;
盗用构造函数
在子类构造函数中调用call()或者apply()方法,并且把this作为上下文,执行父类的构造函数;
- 优势:可以传参给父类
- 缺点:父类原型上的方法不能继承
组合继承
组合继承就是将上面两种方式结合起来的继承,同是也保留了instanceof操作符合isPrototypeOf()方法识别合成对象的能力;
缺点:原型对象上定了很多不必要的属性;
原型式继承
使用场景:你有一个对象,想在它的基础上在创建一个新的对象,这个时候你就可以把这个对象先传给object(),然后对返回的对选哪个做适当的修改。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
这个object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数原型,然后返回这个临时类型的一个实例;本质上,object()是对传入的对象执行了一次浅复制;
ECMAScript5新增了一个Object.create()方法将原型式继承的概念规范化了,接受两个参数:第一个是作为新对象原型的对象,第二个给新对象定义额外属性的对象(可选),用法和Object.defineProperties()的第二个参数一样的;
寄生式继承
思路:类似于寄生构造函数和工厂模式,创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。(一般没有什么用,可以忽略)
寄生式组合继承
这里只调用了一次SuperType构造函数,避免了subType.prototype上不必要也用不到的属性;而且instanceof操作符合isPrototypeOf()方法正常有效;
寄生式组合继承可以算是引用类型继承的最佳模式。
看代码:
// 实现寄生式组合的核心逻辑
function inHeritPrototype(subType,superType) {
let prototype = Object.create(superType.prototype)
// 修正constructor
prototype.constructor = subType
subType.prototype = prototype
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
// 继承实现
inHeritPrototype(SubType,SuperType)
SubType.prototype.sayAge = function() {
console.log(this.age)
}
let instance = new SubType('lsj',18)
instance.sayName() // lsj 调用父类的方法
instance.sayAge() // 18 调用子类的方法
类
类是ECMAScript6中新的基础性语法糖结构,实际上它的背后是的仍然是原型和构造函数的;
类的定义
和函数一样主要有两种定义方式:类声明和类表达式;
与函数的不用点:
- 函数生命可以提前,但是类定义不行;
- 函数受作用域限制,而类受块作用域限制;
类的构成:
- 构造方法
- 实例方法
- 获取函数
- 设置函数
- 静态类方
类的构造函数
方法名字constructor会告诉解析器在使用new操作符创建类的实例时,应该调用真函数;构造函数不是必须的,不定义构造函数就相当于将构造函数定义为空函数;
==实例化==
实例化的过程和前面介绍的构造方法实例化基本一致,只是执行了构造方法为对象添加属性;实例化时候传入的参数给了constructor这个方法,如果不需要参数,则类名后面的括号也是可选的;(一般还是带上吧)
默认情况下,类构造函数会在执行之后返回this对象,也就是实例对象;不过返回的不是this对象而是其他对象,那么这个对象不会通过instanceof操作符检测出跟类有关联;
类构造函数与普通的构造函数主要的区别是:调用构造函数必须使用new操作符;
把类当做特殊函数
可以通过typeof操作符检测类标识符;类型是function
类标签符有prototype属性,原型上面也有一个constructor属性指向自身,也可以使用instanceof操作符检查构造函数原型是否存在实例的原型链中;
重点在于:类定义的constructor方法不会被当成构造函数,在对它使用instanceof操作符会返回false(P253页)
类是javascript的一等公民,可以像函数一样作为参数,也可以像函数一样立即实例化;
实例、原型和类成员
类语法糖可以更方便的定义应该存在于实例上的成员、应该存在于原型上的成员、以及应该存在于类本身的成员。
class Persion {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.loacate = () => {
console.log("instance", this)
}
}
// 定义在原型上
loacate() {
console.log("prototype", this)
}
// 定义在类本身上
static locate() {
console.log("class", this)
}
}
let p = new Persion()
p.loacate()
Persion.prototype.loacate()
Persion.locate()
注意:
- 不能在类块中给原型添加原始值或者对象作为成员数据;
- 类方法等同于对象属性,因此可以使用字符串、符号(symbol)或者计算的值作为键
- 类定义支持获取和设置访问器(get,set方法)
- 类中支持书写生成器方法,可以通过添加一个默认的迭代器,把类变成可迭代对象;
- 虽然类定义不支持显示在原型或类中添加成员数据,但在类定义外部,可以手动添加;
继承
ES6类仅支持单继承,使用extends关键字,就可以继承任何拥有[[constructor]]和原型的对象。不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容);
使用super()的注意事项:
- 1,super只能在派生类构造函数和静态方法中使用;
- 2,不能单独使用super关键字,要么用它调用构造函数,要么用它调用静态方法;
- 3,调用super回调用父类构造函数,并将返回的实例赋值给this;
- 4,super的行为如同调用构造函数,如果需要给父类构造函数传入参数,则需要手动传入;
- 5,如果么有定义构造函数,在实例化派生类时会调用super,而且会传入所有传给派生类参数;
- 6,在类构造函数中,不能再调用super之前应用this;
- 7,如果在派生类中显示定义了构造函数,则要么必须在其中调用super,要么必须在其中返回一个对象;
抽象基类:
假设需要这样一个类,可以供给其他类继承,但是本身不会被实例化;
ECMAScript并没有实现专门支持这种的语法,但是可以通过new.target容易实现。new.target保存通过new关键字调用的类或函数。
可以通过抽象类构造函数进行检查,要求派生类必须实现某一个方法;(P262页)
继承内置类型
ES6类为继承内置引用类型提供了顺畅的时机;
可以这样写:
class SuperArray extends Array {
...
// 自己实现很多方法
}
有些内置类型方法会返回新的实例。默认情况下,返回的实例类型与原始实例的类型是一致的;
如果想覆盖这个默认行为,则可以覆盖Symbol.species访问器,这个访问器决定在创建返回的是实例时使用;
类的混入
看下面代码:
class Vehicle { }
// 定义一个foo混合
let FooMixin = (SuperClass) => {
return class extends SuperClass {
foo() {
console.log("foo")
}
}
}
// 定义一个bar混合
let BarMixin = (SuperClass) => {
return class extends SuperClass {
bar() {
console.log("bar")
}
}
}
// 定义一个bar混合
let BazMixin = (SuperClass) => {
return class extends SuperClass {
baz() {
console.log("baz")
}
}
}
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) { }
let b = new Bus()
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
FooMixin(BarMixin(BazMixin(Vehicle)))这样子嵌套调用,不够优雅。
精进一下:
function mix(baseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), baseClass)
}