Javascript
JS的继承方式
原型链继承
父类的实例作为子类的原型
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
var cat = new Cat();
console.log(cat.name); // cat
cat.eat('fish') // cat正在吃:fish
cat.sleep() // cat正在睡觉!
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
优点:
- 1、非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 2、父类新增原型方法/属性,子类都能访问到
- 3、简单,易于实现
缺点:
- 1、来自原型对象的所有属性被所有实例共享
- 2、创建子实例时,无法向父类构造函数传参
- 3、不支持多继承
- 4、引用值共享
构造继承
使用父类的构造器来增强子类实例,等于是复制父类的实例属性给子类
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
优点:
- 1、解决了
原型链继承中,子类实例共享父类引用属性的问题 - 2、创建子类实例时,可以向父类传递参数(call方法)
- 3、可以实现多继承(call多个父类对象)
缺点:
- 1、实例并不是父类的实例,只是子类的实例
- 2、只能继承父类的实例属性和方法,不能继承原型属性/方法
- 3、无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
实例继承
为父类实例添加新特性,作为子类实例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
var cat = new Cat();
console.log(cat.name) // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
优点:
- 1、不限制调用方式,不管是
new 子类()还是子类(),返回的对象具有相同效果
缺点:
- 1、实例是父类的实例,不是子类的实例
- 2、不支持多继承
拷贝继承
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
this.name = name || 'Tom';
}
var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
优点:
- 1、支持多继承
缺点:
- 1、效率低,内存占用高(因为要拷贝父类的属性)
- 2、无法获取父类不可枚举方法(不可枚举方法,不能使用for in访问到)
组合继承
通过父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
优点:
- 1、弥补了
构造继承的缺陷,可以继承实例属性/方法,也可继承原型属性/方法 - 2、既是子类的实例,也是父类的实例
- 3、不存在引用属性共享问题
- 4、可传参
- 5、函数可复用
缺点:
- 1、调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造时,就不会初始化两次实例方法/属性,避免继承组合的缺点
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
// 创建一个没有实例方法的类
var Super = function () { };
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
// Test Code
var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
装饰器
装饰器是一种函数,写成
@ + 函数名。类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
作用于于同一对象的多个装饰器
- 首先,由上至下依次对装饰器表达式求值,得到返回的真实函数(如果有的话)
- 而后,求值的结果会由下至上依次调用
类的装饰器
function addProp(constructor: Function) {
constructor.prototype.job = 'fe';
}
@addProp
class P {
job: string;
constructor(public name: string) {}
}
let p = new P('林不渡');
console.log(p.job); // fe
类方法的装饰器
function addProps(): MethodDecorator {
return (target, propertyKey, descriptor) => {
console.log(target);
console.log(propertyKey);
console.log(JSON.stringify(descriptor));
descriptor.writable = false;
};
}
class A {
@addProps()
originMethod() {
console.log("I'm Original!");
}
}
const a = new A();
a.originMethod = () => {
console.log("I'm Changed!");
};
a.originMethod(); // I'm Original! 并没有被修改
类属性的装饰器
function addProps(): PropertyDecorator {
return (target, propertyKey) => {
console.log(target);
console.log(propertyKey);
};
}
class A {
@addProps()
originProps: any;
}
参数的装饰器
function paramDeco(params?: any): ParameterDecorator {
return (target, propertyKey, index) => {
console.log(target);
console.log(propertyKey);
console.log(index);
target.constructor.prototype.fromParamDeco = '呀呼!';
};
}
class B {
someMethod(@paramDeco() param1: any, @paramDeco() param2: any) {
console.log(`${param1} ${param2}`);
}
}
new B().someMethod('啊哈', '林不渡!');
// @ts-ignore
console.log(B.prototype.fromParamDeco);
Iterator 和 for…of 循环
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
原生具备 Iterator 接口的数据结构如下
- Array
- Map
- Set
- String
- TypedArray(类型化数组)
- 函数的 arguments 对象
- NodeList 对象
遍历过程
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
将对象变为可迭代对象
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
}
return { value: undefined, done: true };
}
};
}
};
let obj = {
data: {
a: 1,
b: 2
},
[Symbol.iterator]() {
const map = new Map()
let keys = Object.keys(this.data)
keys.forEach(item => {
map.set(item, this.data[item])
})
let i = 0
return {
next: () => {
let mapentries = [...map.entries()]
if (i < map.size) {
return {
value: mapentries[i++],
done: false
}
}
return {
value: undefined,
done: true
}
}
}
}
}
let iter = obj[Symbol.iterator]()
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
方法
Set.prototype.add(value):添加某个值,返回 Set 结构本身。Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。Set.prototype.clear():清除所有成员,没有返回值。
属性
Set.prototype.size:返回Set实例的成员总数。
遍历操作
Set.prototype.keys():返回键名的遍历器Set.prototype.values():返回键值的遍历器Set.prototype.entries():返回键值对的遍历器Set.prototype.forEach():使用回调函数遍历每个成员
Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以
keys方法和values方法的行为完全一致。Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
WeakSet
相同点
WeakSet 结构与 Set 类似,也是不重复的值的集合。
不同点
1、WeakSet 的成员只能是对象,而不能是其他类型的值。
2、WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
用途
WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。(DOM节点)
Map
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
方法
- Map.prototype.set(key, value):
set方法设置键名key对应的键值为value,然后返回整个 Map 结构。 - Map.prototype.get(key):
get方法读取key对应的键值,如果找不到key,返回undefined。 - Map.prototype.has(key):
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。 - Map.prototype.delete(key):
delete方法删除某个键,返回true。如果删除失败,返回false。 - Map.prototype.clear():
clear方法清除所有成员,没有返回值。
遍历操作
Map.prototype.keys():返回键名的遍历器。Map.prototype.values():返回键值的遍历器。Map.prototype.entries():返回所有成员的遍历器。Map.prototype.forEach():遍历 Map 的所有成员。
WeakMap
相同点
WeakMap结构与Map结构类似,也是用于生成键值对的集合。
不同点
1、WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
2、WeakMap的键名所指向的对象,不计入垃圾回收机制。
用途
有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放
e1和e2占用的内存。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。(DOM作键名)
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
Proxy
在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
拦截技巧
将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用。
var object = { proxy: new Proxy(target, handler) };
Proxy 实例也可以作为其他对象的原型对象,实现拦截
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
实例方法
get
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
set
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
apply
apply方法拦截函数的调用、call和apply操作。
apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
has
has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
has()方法可以接受两个参数,分别是目标对象、需查询的属性名。
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
construct
construct()方法返回的必须是一个对象,否则会报错。
construct()方法用于拦截new命令,construct()方法可以接受三个参数。
target:目标对象。args:构造函数的参数数组。newTarget:创造实例对象时,new命令作用的构造函数(下面例子的p)
const p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value
// "called: 1"
// 10
取消访问
Proxy.revocable()方法返回一个可取消的 Proxy 实例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。
Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。
Reflect不是一个函数对象,因此它是不可构造的。
特点
1、将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
2、增加了对Object方法错误的处理
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
3、让Object操作都变成函数行为。
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
4、Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
观察者模式
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
Promise
静态方法
Promise.all
const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
注意,如果作为参数的 Promise 实例,自己定义了
catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
Promise.allSettled
Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。
该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。
results的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。
// 异步操作成功时
{status: 'fulfilled', value: value}
// 异步操作失败时
{status: 'rejected', reason: reason}
Promise.any
只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
Promise.resolve
1、如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
2、thenable对象,指的是具有then方法的对象,比如下面这个对象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});
3、如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。
4、Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
Promise.reject
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。
Promise.try
不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
database.users.get({id: userId})
.then(...)
.catch(...)
但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。
try {
database.users.get({id: userId})
.then(...)
.catch(...)
} catch (e) {
// ...
}
上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。
Generator
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
必须调用遍历器对象的
next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。
yield
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
next
yield表达式本身没有返回值,或者说总是返回undefined。
如果传参,本次next方法所传的参数,就是上一个yield表达式返回的值
实现对象的可迭代
let obj = {
data: {
a: 1,
b: 2
},
[Symbol.iterator]: function*() {
const map = new Map()
let keys = Object.keys(this.data)
keys.forEach(item => {
map.set(item, this.data[item])
})
let i = 0
while (i < map.size) {
yield mapentries[i++]
}
}
}
let iter = obj[Symbol.iterator]()
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
async函数
语法
返回值是一个Promise实例
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
状态变化等到内部所有await命令后面的 Promise 对象执行完
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
await
await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
// 用法
async function one2FiveInAsync() {
for(let i = 1; i <= 5; i++) {
console.log(i);
await sleep(1000);
}
}
one2FiveInAsync();
错误处理
希望即使前一个异步操作失败,也不要中断后面的异步操作
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
代码风格建议
1、await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
2、多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
顶层await
解决模块异步加载的问题。
Past
导入一个异步模块,必须要等到异步操作执行完,才能确保模块的正确导入
awaiting.js除了输出output,还默认输出一个 Promise 对象(async 函数立即执行后,返回一个 Promise 对象),从这个对象判断异步操作是否结束。
// awaiting.js
let output;
export default (async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
// usage.js
import promise, { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
promise.then(() => {
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);
});
Latest
顶层的await命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值
注意,顶层
await只能用在 ES6 模块,不能用在 CommonJS 模块。这是因为 CommonJS 模块的require()是同步加载,如果有顶层await,就没法处理加载了。
// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);