手写源码系列

数组去重

使用双重 forsplice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const unique = arr =>{
for(let i=0;i<arr.length;i++){
for(let j= i+1;j<arr.length;j++){
if(arr[i]===arr[j]){
// 删除第二个
arr.splice(j,1);
// 删除后回调j
j--;
}
}
}
return arr;
}
a=[1,5,4,6,8,2,1,5];
console.log(a);
unique(a);
console.log(a);

使用 indexOfincludes 加新数组

1
2
3
4
5
6
7
8
9
10
11
12
const unique = arr =>{
const uniqueArr = [];
for(let i=0;i<arr.length;i++){
// if (!uniqueArr.includes(arr[i])) {
//includes 检测数组是否有某个值
if(uniqueArr.indexOf(arr[i])=== -1){
//indexof返回-1表示在新数组中不存在该元素
uniqueArr.push(arr[i]);
}
}
return uniqueArr;
}

sort 排序后,使用快慢指针的思想

1
2
3
4
5
6
7
8
9
10
11
12
13
const unique = arr =>{
arr.sort((a,b)=>a-b);//升序排序
let slow = 1,fast =1;//快慢指针起始点都是第二个
while(fast<arr.length){
if(arr[fast] !== arr[fast-1]){
arr[slow++] = arr[fast];
}
fast++;
}
arr.length = slow;// 去重后数组长度,原数组多余部分会被删除
return arr;
}

sort 方法用于从小到大排序(返回一个新数组),其参数中不带以上回调函数就会在两位数及以上时出现排序错误(如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。两位数会变为长度为二的字符串来计算)。

ES6 提供的 Set 去重

1
2
3
4
5
6
7
8
9
/**
* set 集合
* 成员的值都是唯一的
*/
const unique = arr =>{
const res = new Set(arr);
//通过扩展运算符拆分再放入数组中
return [...res];
}

Set 中的元素只会出现一次,即 Set 中的元素是唯一的。

使用哈希表存储元素是否出现(ES6 提供的 map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* map 集合
* 保存键值对,与对象类似,键可以是任意类型
*/
const unique = arr =>{
const map = new Map();
const uniqueArr= new Array();
for(let i =0;i<arr.length;i++){
if(!map.has(arr[i])){
map.set(arr[i],true);
uniqueArr.push(arr[i]);
}
}
return uniqueArr;
}

map 对象保存键值对,与对象类似。但 map 的键可以是任意类型,对象的键只能是字符串类型。

如果数组中只有数字也可以使用普通对象作为哈希表。

filter 配合 indexOf

filter返回过滤后的数组。filter也接收一个函数作为参数,这个函数将作用于数组中的每个元素,根据该函数每次执行后返回的布尔值来保留结果,如果是true就保留,如果是false就过滤掉。

1
2
3
const unique = arr =>{
return arr.filter((item,index,arr) => arr.indexOf(item)===index);
}

这里有可能存在疑问,我来举个例子:

1
2
3
const arr = [1,1,2,1,3]
arr.indexOf(arr[0]) === 0 // 1 的第一次出现
arr.indexOf(arr[1]) !== 1 // 说明前面曾经出现过1

reduce 配合 includes

reduce也是返回一个全新的数组。reduce接受一个函数作为参数,这个函数要有两个形参,代表数组中的前两项,reduce会将这个函数的结果与数组中的第三项再次组成这个函数的两个形参以此类推进行累积操作。

1
2
3
4
5
6
7
const unique = arr =>{
const uniqueArr = arr.reduce((res,cur) =>{
if(!res.includes(cur))res.push(cur);
return res;
},[])// []作为回调函数的第一个参数的初始值
return uniqueArr;
}

如何实现数组扁平化

数组扁平化就是把多维数组转化成一维数组。

ES6提供的新方法 flat(depth)

不会改变原数组

1
2
3
let a = [1,[2,3,[4,[5]]]];  
a.flat(4-1); // [1,2,3,4,5] a是4维数组
a.flat(Infinity); // [1,2,3,4,5] a是4维数组

reduce方法 递归

这里使用的是数组的reduce方法,需要注意的是reduce方法,我们传递了两个参数,
第一个参数就是就是处理扁平化的箭头函数
第二个参数是一个空数组,也是作为遍历的开始。(res)

1
2
3
4
5
6
const flatten = arr =>{
return arr.reduce((res,next)=>{
return res.concat(Array.isArray(next)?flatten(next):next);

},[])
}

模拟实现 new 操作符

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 将该对象的__proto__指向构造函数原型;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this
1
2
3
4
5
6
7
8
9
10
11
12
function new_object() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型 (不推荐使用)
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}

警告: 通过现代浏览器的操作属性的便利性,可以改变一个对象的 [[Prototype]] 属性, 这种行为在每一个JavaScript引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.__proto__ = ... 语句上, 它还会影响到所有继承来自该 [[Prototype]] 的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承 [[Prototype]] 的对象,推荐使用 Object.create()

—MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 优化后 new 实现
function create() {
// 1、通过call,this指向了arguments(封装实参的对象),shift删除并获得arguments中第一个参数——构造函数
// arguments是一个类数组对象,它也可以通过索引来操作数据,也可以获取长度
Con = [].shift.call(arguments);
// 2、创建一个空的对象并将该对象的__proto__指向构造函数原型
let obj = Object.create(Con.prototype);
// 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
let ret = Con.apply(obj, arguments);
// 4、优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
function Car(color) {
this.color = color;
}
Car.prototype.start = function () {
console.log(this.color + " car start");
}
let car = create(Car,"black");

模拟实现 instanceOf

  1. instanceof 判断对象的原型链上是否存在构造函数的原型。只能判断引用类型。
  2. instanceof 常用来判断 A 是否为 B 的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// instanceof 的内部实现 
const instance_of = (Case,Constructor) =>{
// 基本数据类型返回false
// 兼容一下函数对象
if((typeof(Case) !== 'object' && typeof(Case) !== 'function') || Case === null){
return false;
}
// let CaseProto = Object.getPrototypeOf(Case);
let CaseProto = Case.__proto__;
while(true){
// 查到原型链顶端,仍未查到,返回false
if (CaseProto === null) return false;
// 找到相同的原型
if (CaseProto === Constructor.prototype) return true;
// CaseProto = Object.getPrototypeOf(Case);
CaseProto = Case.__proto__;
}
}
let a={};
console.log(instance_of(a,Object))

浅拷贝和深拷贝?

重要: 什么是拷贝?

首先来直观的感受一下什么是拷贝。

1
2
3
4
5
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]

这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。

现在进行浅拷贝:

1
2
3
4
5
let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr);//[1, 2, 3]

当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!

这就是浅拷贝!

但是这又会带来一个潜在的问题:

1
2
3
4
5
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;

console.log(arr);//[ 1, 2, { val: 1000 } ]

咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。

这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。

接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?

浅拷贝

1. 手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 浅拷贝
const shallowClone = source =>{
if(typeof source === 'object' && source !== null){
const output = Array.isArray(source) ? [] : {} ;
for (let key in source){
if(source.hasOwnProperty(key)){
output[key] = source[key];
}
}
return output;
}else{
return source;
}
}

2. Object.assign

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

1
2
3
let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }

3. concat浅拷贝数组

concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

1
2
3
4
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

4. slice浅拷贝

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

5. …展开运算符

1
2
let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果

深拷贝

第一种:JSON.parse(JSON.stringify(object))

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

  1. 无法解决循环引用的问题。举个例子:
1
2
const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  1. 无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等。

  2. 无法拷贝函数(划重点)。

  3. 会忽略 undefined

  4. 会忽略 symbol

就目前而言,第三方最完善的深拷贝方法是 Lodash 库的 _.cloneDeep() 方法了。在实际项目中,如需处理 JSON.stringify() 无法解决的 Case,我会推荐使用它

面试够用版

主要运用到递归的思路去实现一个深拷贝方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 简易版本
const deepClone = source =>{
// 判断是否为数组
const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]';
// 判断是否为引用类型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function');
//拷贝(递归)
const copy = input =>{
// 当输入为函数或基本数据类型时,直接返回
if (typeof input === 'function' || !isObject(input)) return input;

const output = isArray(input) ? [] : {} ;
for(let key in input){
// 如果key是对象的自有属性
if(input.hasOwnProperty(key)){
// 递归调用深拷贝方法
output[key] = copy(input[key]);
}
}
return output;
}
return copy(source);
}
// 测试
let arr = [1, 2, {val: 4}];
let newArr1 = deepClone(arr);//深拷贝
newArr1[2].val = 1000;
let newArr2 = newArr1.slice();//浅拷贝
newArr2[2].val = 2000;


console.log(arr);//[ 1, 2, { val: 1000 } ]
console.log(newArr1);
console.log(newArr2)

需要进一步改进的问题

主要有循环引用、包装对象、函数、原型链、不可枚举属性、Map/WeakMap、Set/WeakSet、RegExp、Symbol、Date、ArrayBuffer、原生 DOM/BOM 对象等。

针对循环引用的问题

以下是一个循环引用(circular reference)的对象:

1
2
const foo = { name: 'Frankie' }
foo.bar = foo

使用WeakMap解决

首先,Map 的键属于强引用,而 WeakMap 的键则属于弱引用。且 WeakMap 的键必须是对象,WeakMap 的值则是任意的。

由于它们的键与值的引用关系,决定了 Map 不能确保其引用的对象不会被垃圾回收器回收的引用。假设我们使用的 Map,那么图中的 foo 对象和我们深拷贝内部的 const map = new Map() 创建的 map 对象一直都是强引用关系,那么在程序结束之前,foo 不会被回收,其占用的内存空间一直不会被释放。

相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。

基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const deepClone = source =>{
// 判断是否为数组
const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]';
// 判断是否为引用类型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function');
//拷贝(递归)
const copy = input =>{
// 当输入为函数或基本数据类型时,直接返回
if (typeof input === 'function' || !isObject(input)) return input;

const output = isArray(input) ? [] : {} ;
for(let key in input){
// 如果key是对象的自有属性
if(input.hasOwnProperty(key)){
// 递归调用深拷贝方法
output[key] = copy(input[key]);
}
}
return output;
}
return copy(source);
}

这里提供另一个思路,也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const deepCopy = source => {
// 其他一样,省略一万字...

// 创建一个数组,将每次拷贝的对象放进去
const copiedArr = []

// 拷贝(递归思路)
const copy = input => {
// 当输入为函数或基本数据类型时,直接返回
if (typeof input === 'function' || !isObject(input)) return input;
// 循环遍历,若有已拷贝过的对象,则直接放回,以解决循环引用的问题
for (let i = 0, len = copiedArr.length; i < len; i++) {
if (input === copiedArr[i].key) return copiedArr[i].value
}

const output = isArray(input) ? [] : {}

// 记录每一次的对象
copiedArr.push({ key: input, value: output })

// 后面的流程不变...
}

return copy(source)
}

解析 call/apply 原理,并手写 call/apply 实现

Function.prototype.call()

call() 方法调用一个函数, 其具有一个指定的 this 值和多个参数(参数的列表)。

1
func.call(thisArg, arg1, arg2, ...)

它运行 func,提供的第一个参数 thisArg 作为 this,后面的作为参数。

看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function sayWord() {
var talk = [this.name, 'say', this.word].join(' ');
console.log(talk);
}

var bottle = {
name: 'bottle',
word: 'hello'
};

// 使用 call 将 bottle 传递为 sayWord 的 this
sayWord.call(bottle);
// bottle say hello

所以,call 主要实现了以下两个功能:

  • call 改变了 this 的指向
  • bottle 执行了 sayWord 函数

模拟实现 call

模拟实现 call 有三步:

  • 将函数设置为对象的属性
  • 执行函数
  • 删除对象的这个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.call = function (context) {
// 将函数设为对象的属性
// 注意:非严格模式下,
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中就是 window 对象)
// 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象(用 Object() 转换)
context = context ? Object(context) : window;
context.fn = this;

// 执行该函数
let args = [...arguments].slice(1);
let result = context.fn(...args);

// 删除该函数
delete context.fn
// 注意:函数是可以有返回值的
return result;
}

Function.prototype.apply()

apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或[类似数组对象)提供的参数。

1
func.apply(thisArg, [argsArray])

它运行 func 设置 this = context 并使用类数组对象 args 作为参数列表。

例如,这两个调用几乎相同:

1
2
func(1, 2, 3);
func.apply(context, [1, 2, 3])

两个都运行 func 给定的参数是 1,2,3。但是 apply 也设置了 this = context

callapply 之间唯一的语法区别是 call 接受一个参数列表,而 apply 则接受带有一个类数组对象。

需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。

模拟实现 apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;

let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}

delete context.fn
return result;
}

解析 bind 原理,并手写 bind 实现

bind()

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

— MDN

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

来个例子说明下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let value = 2;
let foo = {
value: 1
};
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

let bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

通过上述代码可以看出 bind 有如下特性:

  • 1、指定 this
  • 2、传入参数
  • 3、返回一个函数
  • 4、柯里化

模拟实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.bind = function (context) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

// this 指向调用者
var self = this;
// 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
var args = Array.prototype.slice.call(arguments, 1);

// 实现第3点,返回一个函数
return function () {
// 实现第4点,这时的arguments是指bind返回的函数传入的参数
// 即 return function 的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 实现第1点
return self.apply( context, args.concat(bindArgs) );
}
}

但还有一个问题,bind 有以下一个特性:

一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

来个例子说明下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let value = 2;
let foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';

let bindFoo = bar.bind(foo, 'Jack');
let obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

上面例子中,运行结果 this.value 输出为 undefined ,这不是全局 value 也不是 foo 对象中的 value ,这说明 bindthis 对象失效了,new 的实现中生成一个新的对象,这个时候的 this 指向的是 obj

这个可以通过修改返回函数的原型来实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Function.prototype.bind = function (context) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

// this 指向调用者
var self = this;
// 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
var args = Array.prototype.slice.call(arguments, 1);

// 创建一个空对象
var fNOP = function () {};

// 实现第3点,返回一个函数
var fBound = function () {
// 实现第4点,获取 bind 返回函数的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 然后同传入参数合并成一个参数数组,并作为 self.apply() 的第二个参数
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
// 注释1
}

// 注释2
// 空对象的原型指向绑定函数的原型
fNOP.prototype = this.prototype;
// 空对象的实例赋值给 fBound.prototype
fBound.prototype = new fNOP();
return fBound;
}

注释1

  • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true ,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
  • 当作为普通函数时,this 指向 window ,此时结果为 false ,将绑定函数的 this 指向 context

注释2

  • 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend
  • 至于为什么使用一个空对象 fNOP 作为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承),这是因为直接 fBound.prototype = this.prototype 有一个缺点,修改 fBound.prototype 的时候,也会直接修改 this.prototype ;其实也可以直接使用ES5的 Object.create() 方法生成一个新对象,但 bindObject.create() 都是ES5方法,部分IE浏览器(IE < 9)并不支

注意: bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8 及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现

详情可前往 深度解析bind原理、使用场景及模拟实现 查看

补充:柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var add = function(x) {
return function(y) {
return x + y;
};
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。

节流 与 防抖

debouncethrottle 是开发中常用的高阶函数,作用都是为了防止函数被高频调用,换句话说就是,用来控制某个函数在一定时间内执行多少次。

使用场景

比如绑定响应鼠标移动、窗口大小调整、滚屏等事件时,绑定的函数触发的频率会很频繁。若处理函数稍微复杂,需要较多的运算执行时间和资源,往往会出现延迟,甚至导致假死或者卡顿感。为了优化性能,这时就很有必要使用 debouncethrottle 了。

debounce 与 throttle 区别

防抖 (debounce) :多次触发,只在最后一次触发时,执行目标函数。

节流(throttle):限制目标函数调用的频率,比如:1s内不能调用2次。

手写一个 throttle

实现方案有以下两种:

  • 第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环。
  • 第二种方法是使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。

这里我们采用第一种方案来实现,通过闭包保存一个 previous 变量,每次触发 throttle 函数时判断当前时间和 previous 的时间差,如果这段时间差小于等待时间,那就忽略本次事件触发。如果大于等待时间就把 previous 设置为当前时间并执行函数 fn。

我们来一步步实现,首先实现用闭包保存 previous 变量。

1
2
3
4
5
6
7
8
const throttle = (fn, wait) => {
// 上一次执行该函数的时间
let previous = 0
return function(...args) {
console.log(previous)
...
}
}

执行 throttle 函数后会返回一个新的 function ,我们命名为 betterFn

1
2
3
4
const betterFn = function(...args) {
console.log(previous)
...
}

betterFn 函数中可以获取到 previous 变量值也可以修改,在回调监听或事件触发时就会执行 betterFn ,即 betterFn(),所以在这个新函数内判断当前时间和 previous 的时间差即可。

1
2
3
4
5
6
7
8
const betterFn = function(...args) {
let now = +new Date();
if (now - previous > wait) {
previous = now
// 执行 fn 函数
fn.apply(this, args)
}
}

结合上面两段代码就实现了节流函数,所以完整的实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0;
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date();
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now;
fn.apply(this, args);
}
}
}

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

手写一个防抖函数 debounce

防抖函数 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次

实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// fn 是需要防抖处理的函数
// wait 是时间间隔
function debounce(fn, wait = 50) {
// 通过闭包缓存一个定时器 id
let timer = null
// 将 debounce 处理结果当作函数返回
// 触发事件回调时执行这个返回函数
return function(...args) {
// this保存给context
const context = this
// 如果已经设定过定时器就清空上一次的定时器
if (timer) clearTimeout(timer)

// 开始设定一个新的定时器,定时器结束后执行传入的函数 fn
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}

// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

不过 underscore 中的 debounce 还有第三个参数:immediate 。这个参数是做什么用的呢?

传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。(注:并且在 wait 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。

true 传递给 immediate 参数,会让 debouncewait 时间开始计算之前就触发函数(也就是没有任何延时就触发函数),而不是过了 wait 时间才触发函数,而且在 wait 时间内也不会触发(相当于把 fn 的执行锁住)。 如果不小心点了两次提交按钮,第二次提交就会不会执行。

那我们根据 immediate 的值来决定如何执行 fn 。如果是 immediate 的情况下,我们立即执行 fn ,并在 wait 时间内锁住 fn 的执行, wait 时间之后再触发,才会重新执行 fn ,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// immediate 表示第一次是否立即执行
function debounce(fn, wait = 50, immediate) {
let timer = null;
return function(...args) {
// this保存给context
const context = this;
if (timer) clearTimeout(timer)

// immediate 为 true 表示第一次触发后执行
// timer 为空表示首次触发
if (immediate && !timer) {
fn.apply(context, args);
}

timer = setTimeout(() => {
fn.apply(context, args);
}, wait)
}
}

// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000, true)
// 第一次触发 scroll 执行一次 fn,后续只有在停止滑动 1 秒后才执行函数 fn
document.addEventListener('scroll', betterFn)

继承

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
function Parent() {
this.name = 'kk';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child() {

}
Child.prototype = new Parent();
let child1 = new Child();
console.log(child1.getName);//kk

借用构造函数(经典继承)

1
2
3
4
5
6
7
8
9
10
11
function Parent() {
this.name = ['kk','ll'];
}
function Child() {
Parent.call(this);
}
let child2 = new Child();
child2.names.push('yayu');
console.log(child2.names); // ['kk','ll', "yayu"]
let child3 = new Child();
console.log(child3.names); // ['kk','ll']

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {

Parent.call(this, name);

this.age = age;

}

Child.prototype = new Parent();

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

Class实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}

Promise

在传统的异步编程中,如果异步之间存在依赖关系,我们就需要通过层层嵌套回调来满足这种依赖,如果嵌套层数过多,可读性和可维护性都变得很差,产生所谓“回调地狱”,而Promise将回调嵌套改为链式调用,增加可读性和可维护性。

观察者模式

我们先来看一个最简单的Promise使用:

1
2
3
4
5
6
7
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result')
},1000);
})

p1.then(res => console.log(res), err => console.log(err))

观察这个例子,我们分析Promise的调用流程:

  • Promise的构造方法接收一个executor(),在new Promise()时就立刻执行这个executor回调
  • executor()内部的异步任务被放入宏/微任务队列,等待执行
  • then()被执行,收集成功/失败回调,放入成功/失败队列
  • executor()的异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行

其实熟悉设计模式的同学,很容易就能意识到这是个观察者模式,这种收集依赖 -> 触发通知 -> 取出依赖执行 的方式,被广泛运用于观察者模式的实现,在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖

Promise A+规范

上面我们已经简单地实现了一个超低配版Promise,但我们会看到很多文章和我们写的不一样,他们的Promise实现中还引入了各种状态控制,这是由于ES6的Promise实现需要遵循Promise/A+规范,是规范对Promise的状态控制做了要求。Promise/A+的规范比较长,这里只总结两条核心规则:

  1. Promise本质是一个状态机,且状态只能为以下三种:Pending(等待态)Fulfilled(执行态)Rejected(拒绝态),状态的变更是单向的,只能从Pending -> Fulfilled 或 Pending -> Rejected,状态变更不可逆
  2. then方法接收两个可选参数,分别对应状态改变时触发的回调。then方法返回一个promise。then 方法可以被同一个 promise 调用多次。

根据规范,我们补充一下Promise的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._status = PENDING // Promise状态
this._resolveQueue = [] // 成功队列, resolve时触发
this._rejectQueue = [] // 失败队列, reject时触发

// 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
let _resolve = (val) => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = FULFILLED // 变更状态

// 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
// 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 实现同resolve
let _reject = (val) => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = REJECTED // 变更状态
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)
}

// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
}
}

then的链式调用

补充完规范,我们接着来实现链式调用,这是Promise实现的重点和难点

我们思考一下如何实现这种链式调用:

  1. 显然.then()需要返回一个Promise,这样才能找到then方法,所以我们会把then方法的返回值包装成Promise。
  2. .then()的回调需要拿到上一个.then()的返回值
  3. .then()的回调需要顺序执行,以上面这段代码为例,虽然中间return了一个Promise,但执行顺序仍要保证是1->2->3。我们要等待当前Promise状态变更后,再执行下一个then收集的回调,这就要求我们对then的返回值分类讨论
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// then方法
then(resolveFn, rejectFn) {
//return一个新的promise
return new MyPromise((resolve, reject) => {
//把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
//执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
//分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
//这里resolve之后,就能被下一个.then()的回调获取到返回值,从而实现链式调用
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
//把后续then收集的依赖都push进当前Promise的成功回调队列中(_rejectQueue), 这是为了保证顺序调用
this._resolveQueue.push(fulfilledFn)

//reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
this._rejectQueue.push(rejectedFn)
})
}

值穿透 & 状态已变更的情况

我们已经初步完成了链式调用,但是对于 then() 方法,我们还要两个细节需要处理一下

  1. 值穿透:根据规范,如果 then() 接收的参数不是function,那么我们应该忽略它。如果没有忽略,当then()回调不为function时将会抛出异常,导致链式调用中断
  2. 处理状态为resolve/reject的情况:其实我们上边 then() 的写法是对应状态为padding的情况,但是有些时候,resolve/reject 在 then() 之前就被执行(比如Promise.resolve().then()),如果这个时候还把then()回调push进resolve/reject的执行队列里,那么回调将不会被执行,因此对于状态已经变为fulfilledrejected的情况,我们直接执行then回调:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
// 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null

// return一个新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
// 执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

switch (this._status) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case FULFILLED:
fulfilledFn(this._value) // this._value是上一个then回调return的值(见完整版代码)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}

兼容同步任务

完成了then的链式调用以后,我们再处理一个前边的细节,然后放出完整代码。上文我们说过,Promise的执行顺序是new Promise -> then()收集回调 -> resolve/reject执行回调,这一顺序是建立在executor是异步任务的前提上的,如果executor是一个同步任务,那么顺序就会变成new Promise -> resolve/reject执行回调 -> then()收集回调,resolve的执行跑到then之前去了,为了兼容这种情况,我们给resolve/reject执行回调的操作包一个setTimeout,让它异步执行。

这里插一句,有关这个setTimeout,其实还有一番学问。虽然规范没有要求回调应该被放进宏任务队列还是微任务队列,但其实Promise的默认实现是放进了微任务队列,我们的实现(包括大多数Promise手动实现和polyfill的转化)都是使用setTimeout放入了宏任务队列(当然我们也可以用MutationObserver模拟微任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//Promise/A+规定的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._status = PENDING // Promise状态
this._value = undefined // 储存then回调return的值
this._resolveQueue = [] // 成功队列, resolve时触发
this._rejectQueue = [] // 失败队列, reject时触发

// 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
let _resolve = (val) => {
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以兼容executor是同步代码的情况
const run = () => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = FULFILLED // 变更状态
this._value = val // 储存当前value

// 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
// 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// 实现同resolve
let _reject = (val) => {
const run = () => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = REJECTED // 变更状态
this._value = val // 储存当前value
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)
}

// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
// 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null

// return一个新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
// 执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

switch (this._status) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case FULFILLED:
fulfilledFn(this._value) // this._value是上一个then回调return的值(见完整版代码)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}
}

Promise.prototype.catch()

catch()方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。

1
2
3
4
5
//catch方法其实就是执行一下then的第二个回调
catch(rejectFn) {
return this.then(undefined, rejectFn)
}
复制代码

Promise.prototype.finally()

finally()方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。在finally之后,还可以继续then。并且会将值原封不动的传递给后面的then

1
2
3
4
5
6
7
8
//finally方法
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value), // MyPromise.resolve执行回调,并在then中return结果传递给后面的Promise
reason => MyPromise.resolve(callback()).then(() => { throw reason }) // reject同理
)
}
复制代码

PS. 有同学问我MyPromise.resolve(callback())的意义,这里补充解释一下:这个写法其实涉及到一个finally()的使用细节,finally()如果return了一个reject状态的Promise,将会改变当前Promise的状态,这个MyPromise.resolve就用于改变Promise状态,在finally()没有返回reject态Promise或throw错误的情况下,去掉MyPromise.resolve也是一样的(欢迎大家向我提问,勘误的过程中也能很好地加深自己对Promise的理解,大家可以在各个交流群里直接@我)

参考资料:对 Promise.prototype.finally() 的粗浅理解

Promise.resolve()

Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有”then” 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。

1
2
3
4
5
//静态的resolve方法
static resolve(value) {
if(value instanceof MyPromise) return value // 根据规范, 如果参数是Promise实例, 直接return这个实例
return new MyPromise(resolve => resolve(value))
}

Promise.reject()

Promise.reject()方法返回一个带有拒绝原因的Promise对象。

1
2
3
4
//静态的reject方法
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}

Promise.all()

Promise.all(iterable)方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//静态的all方法
static all(promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于处理传入值不为Promise的情况
MyPromise.resolve(p).then(
val => {
index++
result[i] = val
//所有then执行后, resolve结果
if(index === promiseArr.length) {
resolve(result)
}
},
err => {
//有一个Promise被reject时,MyPromise的状态变为reject
reject(err)
}
)
})
})
}

Promise.race()

Promise.race(iterable)方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
for (let p of promiseArr) {
MyPromise.resolve(p).then( //Promise.resolve(p)用于处理传入值不为Promise的情况
value => {
resolve(value) //注意这个resolve是上边new MyPromise的
},
err => {
reject(err)
}
)
}
})
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//Promise/A+规定的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._status = PENDING // Promise状态
this._value = undefined // 储存then回调return的值
this._resolveQueue = [] // 成功队列, resolve时触发
this._rejectQueue = [] // 失败队列, reject时触发

// 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
let _resolve = (val) => {
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以兼容executor是同步代码的情况
const run = () => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = FULFILLED // 变更状态
this._value = val // 储存当前value

// 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
// 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// 实现同resolve
let _reject = (val) => {
const run = () => {
if(this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = REJECTED // 变更状态
this._value = val // 储存当前value
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)
}

// then方法,接收一个成功的回调和一个失败的回调
then(resolveFn, rejectFn) {
// 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null

// return一个新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
const fulfilledFn = value => {
try {
// 执行第一个(当前的)Promise的成功回调,并获取返回值
let x = resolveFn(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}

switch (this._status) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case FULFILLED:
fulfilledFn(this._value) // this._value是上一个then回调return的值(见完整版代码)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}

//catch方法其实就是执行一下then的第二个回调
catch(rejectFn) {
return this.then(undefined, rejectFn)
}

//finally方法
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value), //执行回调,并returnvalue传递给后面的then
reason => MyPromise.resolve(callback()).then(() => { throw reason }) //reject同理
)
}

//静态的resolve方法
static resolve(value) {
if(value instanceof MyPromise) return value //根据规范, 如果参数是Promise实例, 直接return这个实例
return new MyPromise(resolve => resolve(value))
}

//静态的reject方法
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}

//静态的all方法
static all(promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于处理传入值不为Promise的情况
MyPromise.resolve(p).then(
val => {
index++
result[i] = val
if(index === promiseArr.length) {
resolve(result)
}
},
err => {
reject(err)
}
)
})
})
}

//静态的race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
for (let p of promiseArr) {
MyPromise.resolve(p).then( //Promise.resolve(p)用于处理传入值不为Promise的情况
value => {
resolve(value) //注意这个resolve是上边new MyPromise的
},
err => {
reject(err)
}
)
}
})
}
}

Ajax

什么是Ajax和JSON,它们的优缺点

Ajax是全称是Asynchronous JavaScritpt and XML,即异步JavaScript和xml,用于在Web页面中实现异步数据交互,实现页面局部刷新

优点:可以实现异步通信效果,页面局部刷新,带来更好的用户体验。

JSON是一种轻量级的数据交换格式,看着像对象,本质是字符串

优点:轻量级、易于人的阅读和编写,便于js解析,支持复合数据类型。

介绍一下XMLHttpRequest对象,他有哪些常用方法和属性

XMLHttpRequest是Ajax的核心,通过XMLHttpRequest对象,Web开发人员可以在页面加载以后进行页面的局部更新

常用的方法:

  • open(get/post,url,是否异步)创建http请求
  • send()发送请求给服务器
  • setRequestHeader()设置头信息(使用post才会用到,get并不需要调用该方法)

常用的属性:

  • onreadystatechange 用于监听ajax的工作状态(readyState变化时会调用此方法)
  • readyState 用来存放XMLHttpRequest的状态
  • status 服务器返回的状态码
  • responseText 服务器返回的文本内容

说下readyState属性是干嘛的,都有哪几个状态

readyState属性用来存放XMLHttpRequest的状态,监听从0-4发生不同的变化

0:请求未初始化(此时还没有调用open)

1:服务器连接已建立,已经发送请求开始监听

2:请求已接收,已经收到服务器返回的内容

3:请求处理中,解析服务器响应内容

4:请求已完成,且响应就绪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const ajax = url =>{
return new Promise((resolve,reject)=>{
// 创建实例
const xhr = new XMLHttpRequest();
// 返回响应数据的类型
xhr.responseType = 'json';
// 初始化请求
xhr.open("GET",url);
// 发送请求
xhr.send();
// 处理结果
xhr.onreadystatechange = () =>{
if(xhr.readyState === 4){
// 成功
if(xhr.status >= 200 && xhr.status <300){
// 成功结果
resolve(xhr.response);
}else{
reject(xhr.status);
}
}
}

})
}

let btn = document.querySelector('#btn');

btn.addEventListener('click',async function(){
//获取段子信息
let duanzi = await sendAJAX('https://api.apiopen.top/getJoke');
console.log(duanzi);
});

axios封装

  • 前端最流行的 ajax 请求库
  • react/vue 官方都推荐使用 axios 发 ajax 请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function ajax(url,data={},type='GET') {
return new Promise((resolve,reject) => {
let promise;
// 执行异步ajax请求
if(type==='GET'){//发GET请求
promise=axios.get(url,{
params:data
})
}else{//发POST请求
promise=axios.post(url,data)
}
// 如果请求成功了,调用resolve(value)
// 该方法返回一个以response.data值解析后的Promise对象
promise.then(response => {
resolve(response.data)
// console.log(response.data)
// 如果请求失败了,不调用reject(reason),而是提示异常信息
}).catch(error => {
message.error('请求出错了:'+error.message)
})
})

}

高阶函数

什么样的函数是高阶函数那?

至少满足下列一个条件的函数:

  • 接受一个函数或者多个函数作为参数
  • 输出一个函数

JavaScript 中的高阶函数大多都是接受一个函数作为参数,具体传参如下:

1
2
Array.prototype.func = function(callback(currentValue[, index[, array]]){
}[, thisArg])
  • callback:对数组元素进行操作的回调函数
    • currentValue :正在处理的当前元素
    • index:当前元素的索引
    • array:调用高阶函数的数组
  • thisArg:可选,执 行callback 函数时绑定的 this

forEach

用法

forEach主要用于数组的简单遍历,基本使用如下

1
2
3
4
5
6
7
8
arr = [1, 2, 3, 4]
arr.forEach((val, index) => {
console.log(val, index)
})
// 相当于原来的for循环
for (var i = 0; i<arr.length; i++) {
console.log(arr[i], i)
}

模拟实现

我们先来回想一下上面案例中,forEach 内部发生了什么,很简单,就是类似 for 循环一样,运行 arr.length 次回调函数,回调函数的参数是对应的元素索引、元素值和数组本身。那我们就可以模拟出大概的运行流程。

1
2
3
for (var i = 0; i<arr.length; i++) {
callback(arr[i], i, arr)
}

由于 forEach 还可以接受 thisArg 参数作为回调函数的上下文环境,因此使用 call/apply 对上面代码略作修改。

1
callback.call(thisArg, arr[i], i, arr)

通过上面分析,我们就可写出完整的模拟代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Array.prototype.myForEach = function (callbackFn) {
// 判断this是否合法
if (this === null || this === undefined) {
throw new TypeError("Cannot read property 'myForEach' of null");
}
// 判断callbackFn是否合法
if (Object.prototype.toString.call(callbackFn) !== "[object Function]") {
throw new TypeError(callbackFn + ' is not a function')
}
// 取到执行方法的数组对象和传入的this对象
var _arr = this, thisArg = arguments[1] || window;
for (var i = 0; i < _arr.length; i++) {
// 执行回调函数
callbackFn.call(thisArg, _arr[i], i, _arr);
}
}
复制代码

map

用法

map函数对数组的每个元素执行回调函数,并返回含有运算结果的新数组,基本使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const users = [ 
{ name: 'John', age: 34 },
{ name: 'Amy', age: 20 },
{ name: 'camperCat', age: 10 }
];
// 需求:取出users所有人的name,并存放在新数组中
// 不使用map
names = []
for (var i = 0; i<users.length; i++){
names.push(users[i].name)
}

// map是对数组的每一个元素进行操作,因此上述代码可以使用map来进行简化
names = users.map(function (user) {
return user.name
})
// 如果学过箭头函数,还可以进一步简化
names = user.map(user => user.name)

模拟实现

有了上面 forEach 的编写经验,map 只需要稍作修改,使其结果返回新的数组(这里省略掉异常判断)。

1
2
3
4
5
6
7
8
Array.prototype.myMap = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window, res = [];
for (var i = 0; i<_arr.length; i++) {
// 存储运算结果
res.push(callbackFn.call(thisArg, _arr[i], i, _arr));
}
return res;
}

reduce

用法

reduce 与前面的方法有略微的差别:

1
2
arr.reduce(callback(accumulator, currentValue[, index[, array]]){
}[, initialValue])
  • callback:对数组元素进行操作的回调函数
    • accumulator:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue
    • currentValue:正在处理的当前元素
    • 当前元素的索引
    • 调用高阶函数的数组
  • initialValue:作为第一次调用函数的初始值。如果没有提供初始值,则使用数组中的第一个元素。

没有初始值的空数组调用 reduce 会报错

可累加的效果为 reduce 增添了很多精彩,也产生了很多很有用的用途。

  1. 数组累加和
1
2
arr = [0, 1, 2, 3, 4]
arr.reduce((accu, current) => accu + current, 0)
  1. 累加对象数组和

这里如果只是像上面一样使用reduce,最终的结果会存在问题

1
2
3
objArr = [{x: 1}, {x:2}, {x:3}]
objArr.reduce((accu, current) => accu.x + current.x, 0)
复制代码

上述代码返回的结果为NaN,为什么那?

上文提过 accumulator 它的值为上一次调用之后的累计值或初始值,因此第一次调用过后将3赋值给 accumulator ,不再具有 x 属性,因此最终返回 NaN

1
2
3
4
// 法一:先借助map将数值提取出来
objArr.map(obj => obj.x).((accu, current) => accu + current, 0)
// 法二:赋予初值,每次运行accu + current.x
objArr.reduce((accu, current) => accu + current.x, 0)
  1. 计算数组中每个元素出现的次数
1
2
3
4
5
6
7
8
9
10
11
12
var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

var countedNames = names.reduce(function (allNames, name) {
if (name in allNames) {
allNames[name]++;
}
else {
allNames[name] = 1;
}
return allNames;
}, {});
复制代码

模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Array.prototype.myReduce = function(callbackFn) {
var _arr = this, accumulator = arguments[1];
var i = 0;
// 判断是否传入初始值
if (accumulator === undefined) {
// 没有初始值的空数组调用reduce会报错
if (_arr.length === 0) {
throw new Error('initVal and Array.length>0 need one')
}
// 初始值赋值为数组第一个元素
accumulator = _arr[i];
i++;
}
for (; i<_arr.length; i++) {
// 计算结果赋值给初始值
accumulator = callbackFn(accumulator, _arr[i], i, _arr)
}
return accumulator;
}

filter

用法

filter是过滤的意思,它对数组每个元素执行回调函数,返回回调函数执行结果为true的元素。

1
2
3
// 返回偶数
arr = [1, 2, 3, 4, 5];
arr.filter(val => val % 2 == 0)

模拟实现

map 的实现大同小异,map 返回执行回调后所有的元素,而 filter 只返回回调函数执行结果为 true 的元素。

1
2
3
4
5
6
7
8
9
10
Array.prototype.myFilter = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window, res = [];
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为true
if (callbackFn.call(thisArg, _arr[i], i, _arr)) {
res.push(_arr[i]);
}
}
return res;
}

every

用法

every并不返回数组,返回布尔值 true/false ,数组的每个元素执行回调函数,如果执行结果全为 trueevery 返回 true,否则返回 false

1
2
3
4
5
6
7
arr = [1, 3, 5, 7, 8, 9]
// false,8为偶数,不满足
arr.every(ele => ele % 2 == 1)

arr2 = [2, 4, 6]
// true 都是偶数
arr2.every(ele => ele % 2 == 0)

模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array.prototype.myEvery = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window;
// 开始标识值为true
// 遇到回调返回false,直接返回false
// 如果循环执行完毕,意味着所有回调返回值为true,最终结果为true
var flag = true;
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为false,函数中断
if (!callbackFn.call(thisArg, _arr[i], i, _arr)) {
return false;
}
}
return flag;
}

some

用法

someevery 功能类似,都是返回布尔值。只要回调函数结果有一个 truesome便返回 true,否则返回 false

模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array.prototype.mySome = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window;
// 开始标识值为false
// 遇到回调返回true,直接返回true
// 如果循环执行完毕,意味着所有回调返回值为false,最终结果为false
var flag = false;
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为false,函数中断
if (callbackFn.call(thisArg, _arr[i], i, _arr)) {
return true;
}
}
return flag;
}

find/findIndex

用法

findfindIndexES6 新添加的数组方法,返回满足回调函数的第一个数组元素/数组元素索引。当数组中没有能满足回调函数的元素时,会分别返回 undefined和-1

1
2
3
4
5
const users = [ 
{ name: 'John', age: 34 },
{ name: 'Amy', age: 20 },
{ name: 'camperCat', age: 10 }
];
  1. 返回name为John的年龄

    在没有find方法时,实现类似效果,需要循环遍历,查找到name=Jonn后,找到年龄。但使用find就可以轻松快捷的实现。

    1
    JohnAge = users.find(user => user.name === 'John').age
  2. 返回name为Amy的索引

    ES6以前Array提供了查找数组中元素的方法:indexOf,lastIndexOf,但是这两个方法在查找对象时都无能为力。

    1
    2
    3
    4
    // 返回值为-1,说明未查到Amy
    users.indexOf({ name: 'Amy', age: 20 })
    // 返回hi为1,成功查到Amy
    users.findIndex(user => user.name === 'Amy')

    indexOf也可以用来查找数组中是否存在某元素,但其语义化并不好,每次需要与 -1 进行比较,因此 ES6 添加了新的 includes 方法。

模拟实现

find/findIndex 都是寻找到第一个满足回调函数的元素返回,上面我们学习的 some 也是类似机制,因此它们的原生代码类似。

1
2
3
4
5
6
7
8
9
10
11
12
Array.prototype.myFind = function(callbackFn) {
var _arr = this, thisArg = arguments[1] || window;
// 遇到回调返回true,直接返回该数组元素
// 如果循环执行完毕,意味着所有回调返回值为false,最终结果为undefined
for (var i = 0; i<_arr.length; i++) {
// 回调函数执行为false,函数中断
if (callbackFn.call(thisArg, _arr[i], i, _arr)) {
return _arr[i];
}
}
return undefined;
}