js中手写常用方法
# 关键相关
# 手写call
call 和 apply 的区别:call 方法接收的是一个参数列表,apply 方法接收的是一个包含多个参数的数组。
- 1.
context
存在就使用context
,否则是window
- 2.使用
Object(context)
将context
转换成对象,并通过context.fn
将this
指向context
- 3.循环参数,注意从
1
开始,第0
个是上下文,后面才是我们需要的参数 - 4.将参数字符串
push
进args
- 5.字符串和数组拼接时,数组会调用
toString
方法,这样可以实现将参数一个个传入,并通过eval
执行 - 6.拿到结果返回前,删除掉
fn
Function.prototype.call = function(context) {
context = context ? Object(context) : window;
context.fn = this;
let args = [];
for (let i = 1; i < arguments.length; i++) {
args.push('arguments['+ i +']');
}
let res = eval('context.fn('+ args +')');
delete context.fn;
return res;
}
2
3
4
5
6
7
8
9
10
11
Object.prototype.myCall = function (context, ...params) {
if (typeof this !== 'function') {
throw TypeError('holp caller is functiom')
}
let fn = Symbol('fn')
let obj = context || window
obj[fn] = this
let res = obj[fn](...params)
delete obj[fn]
return res
}
2
3
4
5
6
7
8
9
10
11
# 手写apply
- 1.
apply
无需循环参数列表,传入的args
就是数组 - 2.但是
args
是可选参数,如果不传入的话,直接执行
Function.prototype.apply = function(context, args) {
context = context ? Object(context) : window;
context.fn = this;
if (!args) {
return context.fn();
}
let res = eval('context.fn('+ args +')');
delete context.fn;
return res;
}
2
3
4
5
6
7
8
9
10
Object.prototype.myApply = function (context, params = []) {
if (typeof this !== 'function') {
throw TypeError('holp caller is functiom')
}
let fn = Symbol('fn')
let obj = context || window
obj[fn] = this
let res = obj[fn](...params)
delete obj[fn]
return res
}
2
3
4
5
6
7
8
9
10
11
# 手写bind
- 1.
bind
的参数可以在绑定和调用的时候分两次传入 - 2.
bindArgs
是绑定时除了第一个参数以外传入的参数,args
是调用时候传入的参数,将二者拼接后一起传入 - 3.如果使用
new
运算符构造绑定函数,则会改变this
指向,this
指向当前的实例 - 4.通过
Fn
链接原型,这样fBound
就可以通过原型链访问父类Fn
的属性
Function.prototype.bind = function(context) {
let that = this;
let bindArgs = Array.prototype.slice.call(arguments, 1);
function Fn () {};
function fBound(params) {
let args = Array.prototype.slice.call(arguments) ;
return that.apply(this instanceof fBound ? this : context, bindArgs.concat(args));
}
Fn.prototype = this.prototype;
fBound.prototype = new Fn();
return fBound;
}
2
3
4
5
6
7
8
9
10
11
12
Object.prototype.myBind = function (context, ...arg) {
if (typeof this !== 'function') {
throw TypeError('holp caller is functiom')
}
let obj = context || window
return (...arg2) => {
this.call(obj, ...arg, ...arg2)
}
}
2
3
4
5
6
7
8
9
# 手写instanceOf
- 1.在
left
的原型链中层层查找,是否有原型等于prototype
- 2.确定边界条件,如果
left === null
,即找到头没找到返回false
,right === left
,即找到返回true
- 3.
left = left.__proto__
,不停的向上查找
const myInstanceof = function(left, right) {
right = right.prototype;
left = left.__proto__;
while (true) {
if (left === null) {
return false;
}
if (right === left) {
return true;
}
left = left.__proto__;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
function instance(L, R) {
let l = L.__proto__
let r = R.prototype
while (true) {
if (l === null) return false
if (l === r) return true
l = l.__proto__
}
}
2
3
4
5
6
7
8
9
# 手写Objeact.create
- 新建一个空的构造函数
F
,然后让F.prototype
指向obj
,最后返回F
的实例
const myCreate = function (obj) {
function F() {};
F.prototype = obj;
return new F();
}
2
3
4
5
function create(obj) {
function F() { }
F.prototype = obj
return new F()
}
2
3
4
5
# 手写new
- 1.
Constructor
就是new
时传入的第一个参数,剩余的arguments
是其他的参数 - 2.使用
obj.__proto__ = Constructor.prototype
继承原型上的方法 - 3.将剩余的
arguments
传给Contructor
,绑定this
指向为obj
,并执行 - 4.如果构造函数返回的是引用类型,直接返回该引用类型,否则返回
obj
const myNew = function() {
let Constructor = Array.prototype.shift.call(arguments);
let obj = {};
obj.__proto__ = Constructor.prototype;
let res = Constructor.apply(obj, arguments);
return res instanceof Object ? res : obj;
}
2
3
4
5
6
7
function myNew(context, ...params) {
let obj = Object.create(context.prototype)
let res = context.apply(obj, params)
return typeof res === 'object' ? res : obj
}
2
3
4
5
# 深拷贝
function deepClone(p, c) {
for (let prop in p) {
if (typeof p[prop] === 'object') {
c[prop] = p[prop] instanceof Array ? [] : {}
deepClone(p[prop], v[prop])
} else {
c[prop] = p[prop]
}
}
}
2
3
4
5
6
7
8
9
10
# es5继承
function Animal(name) {
this.name = name
}
Animal.prototype.sayName = function () {
console.log(this.name)
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = "Dog"
Dog.prototype.sayAge = function () {
console.log(this.age)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# promise
function myPromise(constructor) {
let that = this
this.status = 'pendding'
this.succVal = ''
this.errVal = ''
function resovle(val) {
if (that.status === 'pendding') {
that.succVal = val
that.status = 'resolved'
}
}
function reject(val) {
if (that.status === 'pendding') {
that.errVal = val
that.status = 'rejected'
}
}
try {
constructor(resovle, reject)
} catch (error) {
reject(error)
}
}
myPromise.prototype.then = function (succFn, errFn) {
let that = this
switch (that.status) {
case 'resolved': succFn(that.succVal)
break
case 'rejected': errFn(that.errVal)
break
}
}
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
# 手撕new
function new (Func, ...arg){
let obj = {} //定义了一个对象。
obj.__proto__ = Func.prototype //将Func.prototype赋值为对象的__proto__属性,即原型链的概念
let res = Func.call(obj, ...arg) //更改Func的this指向
return res instanceof Object ? res : obj
}
2
3
4
5
6
通过这段代码我们可以看得出来,当我们new一个对象的时候,系统都做了些什么事情。我们可以测试一下,因为new是关键字,我们先改个函数名。
function Feng (Func, ...arg){
let obj = {} //定义了一个对象。
obj.__proto__ = Func.prototype //将Func.prototype赋值为对象的__proto__属性,即原型链的概念
let res = Func.call(obj, ...arg) //更改Func的this指向
return res instanceof Object ? res : obj
}
new Feng(Array,[1,2,3])
2
3
4
5
6
7
# 手撕函数柯里化
const curry = ( fn, arr = []) => (...args) => ( a => a.length === fn.length? fn(...a) : curry(fn, a))([...arr, ...args])
let curryPlus = curry((a,b,c,d)=>a+b+c+d)
curryPlus(1,2,3)(4) //返回10
curryPlus(1,2)(4)(3) //返回10
curryPlus(1,2)(3,4) //返回10
2
3
4
5
6
# 手撕Promise
面试的时候经常会遇到让你手写promise的面试题,大多人都手足无措,其实手撕promise没有那么难。
首先,先看一下promise的用法:
new Promise((resolve, reject) => {
resolve('hello'); // or reject('hello')
})
.then(res => {})
.catch(err => {})
//也可以写为:
let executor = (resolve, reject) => {
resolve('hello'); // or reject('hello')
}
new Promise(executor)
.then(res => {})
.catch(err => {})
2
3
4
5
6
7
8
9
10
11
12
13
分析一下promise的逻辑:
- Promise 是一个类 exectuor 是一个立即执行函数。
- 有三个状态 默认是【PENDING】 FULFILED (成功态)、REJECTED(失败态),状态一经改变不能再修改。
- resolve和reject的结果传入到then中的回调函数中。
- 发布订阅模式实现异步执行。
- 如果promise返回一个普通值(不论是 resolve 还是reject中返回的)都传递到下一个then的成功中。
- 如果返回一个错误 一定走到下一次的失败。
- 如果返回的是一个promise 会采用promise的状态决定下一次的成功还是失败,如果离自己最近的then没有错误处理,会向下找。
- 每次执行promise.then 都会返回一个'全新的promise'。
# Promise 基本结构
class Promise {
constructor(executor) {
// 定义 resolve
let resolve = res => {}
// 定义 reject
let reject = err => {}
// 自动执行
executor(resolve, reject);
}
}
2
3
4
5
6
7
8
9
10
# Promise 三种状态实现
Promise有三种状态:
- pending [待定] 初始状态
- fulfilled [实现] 操作成功
- rejected [被否决] 操作失败
promise 状态有如下特点:
1.promise 对象初始化状态为 pending。
2.当调用resolve(成功),会由pending => fulfilled。
3.当调用reject(失败),会由pending => rejected。
class Promise {
constructor(executor) {
this.status = "pending"; // 默认状态
this.value; // resolve 成功时的值
this.error; // reject 失败时的值
let resolve = res => {
if(this.status === "pending") {
this.value = res;
this.status = "resolved";
}
}
let reject = err => {
if(this.status === "pending") {
this.error = err;
this.status = "rejected";
}
}
executor(resolve, reject);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Promise 对象方法 then 实现
then接受两个回调:onFulfilled(成功的回调),onRejected(失败的回调)
promise.then(onFulfilled, onRejected);
1.onFulfilled(onResolved):可选参数,如果不是函数则必须忽略。
2.onRejected:可选参数,如果不是函数则必须忽略。
3.当promise成功执行,所有onFulfilled按注册顺序执行,如果promise被拒绝,所有onRejected按注册顺序执行。
4.onFulfilled 和 onRejected必须作为纯函数调用。
5.promise的executor执行完毕并调用resolve或reject方可调用then参数onFulfilled 和 onRejected。
6.无论promise状态是resolved还是rejected,只要还有未执行onFulfilled,onRejected或catch(只处理reject状态)存在且调用,返回的promise均为resolved状态。
class Promise {
constructor(executor) {
this.status = "pending"; // 默认promise状态
this.value; // resolve成功时的值
this.error; // reject失败时的值
let resolve = res => {
if(this.status === "pending") {
this.value = res;
this.status = "resolved";
}
}
let reject = err => {
if(this.status === "pending") {
this.error = err;
this.status = "rejected";
}
}
executor(resolve, reject)
}
// 声明 then
then(onFullfilled, onRejected) {
if(this.status === "resolved") {
onFullfilled(this.value)
}
if(this.status === "rejected") {
onRejected(this.error)
}
}
}
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
我们增加了onFullfilled和onRejected,用来储存我们在then中回调。then中可传递一个成功和一个失败的回调,当pending的状态变为resolve时执行成功回调,当pending的状态变为reject或者出错时则执行失败的回调。
# Promise异步实现
当 resolve 在 setTimeout
内执行,then函数执行 时 state 还是 pending 等待状态。我们就需要在 then 调用的时候,将成功和失败存到各自的数组,一旦 reject 或者 resolve,就调用它们。
类似于分布订阅,先将 then 内的两个函数存储,由于 promise 可以有多个 then,所以存在同一个数组内。当成功或失败的时候用 forEach 调用他们。
class Promise {
constructor(executor) {
this.status = "pending"; // 默认promise状态
this.value; // resolve成功时的值
this.error; // reject失败时的值
this.resolveQueue = []; // 成功存放的数组
this.rejectQueue = []; // 失败存放法数组
let resolve = value => {
if(this.status === "pending") {
this.value = value;
this.status = "resolved";
// 一旦resolve执行,调用成功数组的函数
this.resolveQueue.forEach(fn => fn());
}
}
let reject = value => {
if(this.status === "pending") {
this.error = value;
this.status = "rejected";
}
// 一旦reject执行,调用失败数组的函数
this.rejectQueue.forEach(fn => fn());
}
executor(resolve, reject)
}
// 执行到then的时候
then(onFullfilled, onRejected) {
if(this.status === "resolved") {
this.resolveQueue.push(() => {
onFullfilled(this.value);
})
}
if(this.status === "rejected") {
this.rejectQueue.push(() => {
onRejected(this.error);
})
}
// 当状态state为pending时
if(this.status === "pending") {
// onFulfilled传入到成功数组
this.resolveQueue.push(() => {
onFullfilled(this.value);
})
// onRejected传入到失败数组
this.rejectQueue.push(() => {
onRejected(this.error);
})
}
}
}
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
# then 的链式调用
链式调用,也就是我们经常看到的new Promise().then().then()
这样的写法,可以解决地狱回调。
class Promise {
constructor(executor) {
this.status = "pending"; // 默认promise状态
this.value; // resolve成功时的值
this.error; // reject失败时的值
this.resolveQueue = []; // 成功时回调队列
this.rejectQueue = []; // 失败时回调队列
let resolve = value => {
if(this.status === "pending") {
this.value = value;
this.status = "resolved";
this.resolveQueue.forEach(fn => fn())
}
}
let reject = value => {
if(this.status === "pending") {
this.error = value;
this.status = "rejected";
this.rejectQueue.forEach(fn => fn())
}
}
executor(resolve, reject)
}
then(onFullfilled, onRejected) {
let promise2;
promise2 = new Promise((resolve, reject) => {
if(this.status === "resolved") {
let x = onFullfilled(this.value);
// resolvePromise函数,处理自己return的promise和默认的promise2的关系
resolvePromise(promise2, x, resolve, reject);
}
if(this.status === "rejected") {
let x = onRejected(this.value);
resolvePromise(promise2, x, resolve, reject);
}
if(this.status === "pending") {
this.resolveQueue.push(() => {
let x = onFullfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
})
this.rejectQueue.push(() => {
let x = onRejected(this.error);
resolvePromise(promise2, x, resolve, reject);
})
}
});
// 返回 promise,达成链式效果
return promise2;
}
}
/**
* 处理promise递归的函数
*
* promise2 {Promise} 默认返回的promise
* x {*} 我们自己 return 的对象
* resolve
* reject
*/
function resolvePromise(promise2, x, resolve, reject){
// 循环引用报错
if(x === promise2){
// reject 报错抛出
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 锁,防止多次调用
let called;
// x 不是 null 且 x 是对象或者函数
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
// A+ 规定,声明then = x的then方法
let then = x.then;
// 如果then是函数,就默认是promise了
if (typeof then === 'function') {
// then 执行 第一个参数是 this 后面是成功的回调 和 失败的回调
then.call(x, y => {
// 成功和失败只能调用一个
if (called) return;
called = true;
// 核心点2:resolve 的结果依旧是 promise 那就继续递归执行
resolvePromise(promise2, y, resolve, reject);
}, err => {
// 成功和失败只能调用一个
if (called) return;
called = true;
reject(err);// 失败了就失败了
})
} else {
resolve(x); // 直接成功即可
}
} catch (e) { // 走到 catch 也属于失败
if (called) return;
called = true;
// 取then出错了那就不要在继续执行了
reject(e);
}
} else {
resolve(x);
}
}
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
# 值穿透调用
什么是值穿透,就是当执行多个 then,我们期望最后那个 then 打印出 'Feng'。
new Promise((resolve, reject)=>{
resolve('Feng');
}).then().then().then().then().then().then().then((res)=>{
console.log(res);
})
2
3
4
5
实现很简单:onFulfilled 如果不是函数,就忽略 onFulfilled,直接返回 value。
相应的,我们也要处理下没有 onRejected 的情况:onRejected 如果不是函数,就忽略 onRejected,直接扔出错误。
class Promise {
constructor(executor) {
this.status = "pending"; // 默认promise状态
this.value; // resolve成功时的值
this.error; // reject失败时的值
this.resolveQueue = []; // 成功时回调队列
this.rejectQueue = []; // 失败时回调队列
let resolve = value => {
if(this.status === "pending") {
this.value = value;
this.status = "resolved";
this.resolveQueue.forEach(fn => fn())
}
}
let reject = value => {
if(this.status === "pending") {
this.error = value;
this.status = "rejected";
this.rejectQueue.forEach(fn => fn())
}
}
executor(resolve, reject)
}
then(onFullfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err;}
let promise2;
promise2 = new Promise((resolve, reject) => {
if(this.status === "resolved") {
let x = onFullfilled(this.value);
// resolvePromise函数,处理自己return的promise和默认的promise2的关系
resolvePromise(promise2, x, resolve, reject);
}
if(this.status === "rejected") {
let x = onRejected(this.value);
resolvePromise(promise2, x, resolve, reject);
}
if(this.status === "pending") {
this.resolveQueue.push(() => {
let x = onFullfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
})
this.rejectQueue.push(() => {
let x = onRejected(this.error);
resolvePromise(promise2, x, resolve, reject);
})
}
});
// 返回 promise,达成链式效果
return promise2;
}
}
/**
* 处理promise递归的函数
*
* promise2 {Promise} 默认返回的promise
* x {*} 我们自己 return 的对象
* resolve
* reject
*/
function resolvePromise(promise2, x, resolve, reject){
// 循环引用报错
if(x === promise2){
// reject 报错抛出
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 锁,防止多次调用
let called;
// x 不是 null 且 x 是对象或者函数
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
// A+ 规定,声明then = x的then方法
let then = x.then;
// 如果then是函数,就默认是promise了
if (typeof then === 'function') {
// then 执行 第一个参数是 this 后面是成功的回调 和 失败的回调
then.call(x, y => {
// 成功和失败只能调用一个
if (called) return;
called = true;
// 核心点2:resolve 的结果依旧是 promise 那就继续递归执行
resolvePromise(promise2, y, resolve, reject);
}, err => {
// 成功和失败只能调用一个
if (called) return;
called = true;
reject(err);// 失败了就失败了
})
} else {
resolve(x); // 直接成功即可
}
} catch (e) { // 走到 catch 也属于失败
if (called) return;
called = true;
// 取then出错了那就不要在继续执行了
reject(e);
}
} else {
resolve(x);
}
}
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
# Promise 对象方法 catch
catch 是失败的回调,相当于执行 this.then(null,fn)。
class Promise {
constructor(executor) {
this.status = "pending"; // 默认promise状态
this.value; // resolve成功时的值
this.error; // reject失败时的值
this.resolveQueue = []; // 成功时回调队列
this.rejectQueue = []; // 失败时回调队列
let resolve = value => {
if(this.status === "pending") {
this.value = value;
this.status = "resolved";
this.resolveQueue.forEach(fn => fn())
}
}
let reject = value => {
if(this.status === "pending") {
this.error = value;
this.status = "rejected";
this.rejectQueue.forEach(fn => fn())
}
}
executor(resolve, reject)
}
then(onFullfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err;}
let promise2;
promise2 = new Promise((resolve, reject) => {
if(this.status === "resolved") {
let x = onFullfilled(this.value);
// resolvePromise函数,处理自己return的promise和默认的promise2的关系
resolvePromise(promise2, x, resolve, reject);
}
if(this.status === "rejected") {
let x = onRejected(this.value);
resolvePromise(promise2, x, resolve, reject);
}
if(this.status === "pending") {
this.resolveQueue.push(() => {
let x = onFullfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
})
this.rejectQueue.push(() => {
let x = onRejected(this.error);
resolvePromise(promise2, x, resolve, reject);
})
}
});
// 返回 promise,达成链式效果
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected)
}
}
/**
* 处理promise递归的函数
*
* promise2 {Promise} 默认返回的promise
* x {*} 我们自己 return 的对象
* resolve
* reject
*/
function resolvePromise(promise2, x, resolve, reject){
// 循环引用报错
if(x === promise2){
// reject 报错抛出
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 锁,防止多次调用
let called;
// x 不是 null 且 x 是对象或者函数
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
// A+ 规定,声明then = x的then方法
let then = x.then;
// 如果then是函数,就默认是promise了
if (typeof then === 'function') {
// then 执行 第一个参数是 this 后面是成功的回调 和 失败的回调
then.call(x, y => {
// 成功和失败只能调用一个
if (called) return;
called = true;
// 核心点2:resolve 的结果依旧是 promise 那就继续递归执行
resolvePromise(promise2, y, resolve, reject);
}, err => {
// 成功和失败只能调用一个
if (called) return;
called = true;
reject(err);// 失败了就失败了
})
} else {
resolve(x); // 直接成功即可
}
} catch (e) { // 走到 catch 也属于失败
if (called) return;
called = true;
// 取then出错了那就不要在继续执行了
reject(e);
}
} else {
resolve(x);
}
}
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
# 框架相关
# vue2双向绑定
function bothwayBind(fromKey, container, obj, key) {
Object.defineProperty(obj, key, {
set(val) {
fromKey.value = val
container.innerHTML = val
}
})
fromKey.onkeyup = function (e) {
obj[key] = e.target.value
}
}
2
3
4
5
6
7
8
9
10
11
# vue3双向绑定
# 简介
在vue2中,实现双向绑定,是使用Object.defineProperty()方法对属性设置get和set方法实现的。而Vue3的双向绑定则使用了es6中的proxy对象。
Proxy
翻译过来就是代理的意思,何为代理呢?就是 用 new
创建一个目标对象(traget
)的虚拟化对象,然后代理之后就可以拦截JavaScript
引擎内部的底层对象操作;这些底层操作被拦截后会触发响应特定操作的陷阱函数。
基本用法:
const p = new Proxy(target, handler)
target
要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为。
我们看一下Proxy是如何使用的:
// 定义一个空对象
let data = {};
// 创建一个 Proxy , 将 data 作为目标对象
let proxy = new Proxy(data, {});
// 修改Proxy 代理对象的name属性
proxy.name = '枫';
console.log(proxy); // { name: '枫' }
console.log(data); // { name: '枫' }
2
3
4
5
6
7
8
9
handler
对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy
的各个捕获器(trap)。
参数
作用
handler.get()
属性读取
操作的捕捉器。
handler.set()
属性设置
操作的捕捉器。
handler.set
handler.set()
方法用于拦截设置属性值的操作。
// 定义一个对象
let data = {
name: "samy",
age: '28'
};
// 创建一个 Proxy , 将 data 作为目标对象
let p = new Proxy(data, {
set(target, prop, value) {
// target = 目标对象
// prop = 设置的属性
// value = 修改后的值
console.log(target, prop, value)
// { name: 'samy', age: '28' } 'age' 18
return Reflect.set(...arguments)
}
});
p.age = 18;
console.log(data)
//{name: "samy", age: "18"}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们修改p的时候出发了set方法,获取到了修改后的属性,接下来我们需要把修改后的值赋给data。
加了一行代码: return Reflect.set(...arguments)。
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API 。我们需要在 handler.set()
中 return
一个 Reflect.set(...arguments)
来进行赋值给目标对象。
Reflect.set
Reflect.set
方法设置target
对象的name
属性等于value
。
// 定义一个对象
let data = {
name: "枫",
age: '23'
};
// 创建一个 Proxy , 将 data 作为目标对象
let p = new Proxy(data, {
set(target, prop, value) {
// target = 目标对象
// prop = 设置的属性
// value = 修改后的值
console.log(target, prop, value)
// { name: '枫', age: '23' } 'age' 18
return Reflect.set(...arguments)
}
});
// 直接修改p就可以了
p.age = 18;
console.log(data)
//{name: "枫", age: "18"}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 实现
拿到参数,进行代理,数据渲染。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./src/index.js"></script>
</head>
<body>
<div id="app">{{name}}
<h2>{{age}}</h2>
<input type="text" v-model="name">
{{name}}
</div>
<script>
let vm = new Reactive({
el: "#app",
data: {
name: "samy",
age: 18,
}
});
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
index.js
// EventTarget [6]
class Reactive extends EventTarget {
// 接收参数
constructor(options) {
super();
this.options = options;
// data 赋值
this.$data = this.options.data;
// 挂载元素
this.el = document.querySelector(this.options.el);
// 调用 compile 函数
this.compile(this.el);
// 调用双向绑定
this.observe(this.$data);
}
// 双向绑定
observe(data) {
// 备份this
let _this = this;
// 接收目标对象进行代理
this.$data = new Proxy(data, {
set(target, prop, value) {
// 创建一个自定义事件 CustomEvent [5]
// 事件名称使用的是 prop
let event = new CustomEvent(prop, {
// 传入新的值
detail: value
})
// 派发 event 事件
_this.dispatchEvent(event);
return Reflect.set(...arguments);
}
})
}
// 渲染数据
compile(el) {
// 获取el的子元素
let child = el.childNodes;
// 遍历判断是否存在文本
[...child].forEach(node => {
// 如果node的类型是TEXT_NODE
if (node.nodeType === 3) {
// 拿到文本内容
let txt = node.textContent;
// 正则匹配
let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
if (reg.test(txt)) {
let $1 = RegExp.$1;
this.$data[$1] && (node.textContent = txt.replace(reg, this.$data[$1]))
// 绑定自定义事件
this.addEventListener($1, e => {
// 替换成传进来的 detail
node.textContent = txt.replace(reg, e.detail)
})
}
// 如果node的类型是ELEMENT_NODE
} else if (node.nodeType === 1) {
// 获取attr
let attr = node.attributes;
// 判断是否存在v-model属性
if (attr.hasOwnProperty('v-model')) {
// 获取v-model中绑定的值
let keyName = attr['v-model'].nodeValue;
// 赋值给元素的value
node.value = this.$data[keyName]
// 绑定事件
node.addEventListener('input', e => {
// 当事件触发的时候我们进行赋值
this.$data[keyName] = node.value
})
}
// 递归执行
this.compile(node)
}
})
}
}
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
# 算法相关
# 二分查找
function binarySearch(arr, start, end, value) {
if (start > end) {
return -1
}
let middle = parseInt((start + end) / 2)
if (value < arr[middle]) {
end = middle - 1
return binarySearch(arr, start, end, value)
} else if (value > arr[middle]) {
start = middle + 1
return binarySearch(arr, start, end, value)
} else if (value === arr[middle]) {
return middle
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 快排
function quickSort(arr) {
if (arr.length <= 1) { return arr; }
let middle = Math.floor(arr / 2)
let value = arr.splice(middle, 1)[0]
let left = [], right = []
for (let i = 0; i < arr.length; i++) {
if (arr[i] > value) {
right.push(arr[i])
} else if (arr[i] < value) {
left.push(arr[i])
}
}
return quickSort(left).concat([value], quickSort(right))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 冒泡排序
function bubbleSort(arr) {
let len = arr.length
let temp
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] < arr[j]) {
temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
}
return arr
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 选择排序
function selectSort(arr) {
let len = arr.length
let minIndex, temp
for (let i = 0; i < len; i++) {
minIndex = i
for (let j = i + 1; j < len; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j
}
}
temp = arr[minIndex]
arr[minIndex] = arr[i]
arr[i] = temp
}
return arr
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 原生Ajax
let xhr = new XMLHttpRequest()
xhr.open(Get,'xxx.js',false)
xhr.onreadystatechange = function () {
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.responseText)
}
}
2
3
4
5
6
7
# 订阅发布模式
let Observer = (function () {
let __msg = {}
return {
register: function (type, fn) {
if (typeof __msg[type] === 'undefined') {
__msg[type] = [fn]
} else {
__msg[type].push(fn)
}
},
dispatch: function (type, args) {
if (!__msg[type]) return
let len = __msg[type].length
let params = { type, args }
for (let i = 0; i < len; i++) {
__msg[type][i].call(this, params)
}
},
remove: function (type, fn) {
if (__msg[type] instanceof Array) {
let len = __msg[type].length - 1
for (let i = len; i >= 0; i--) {
__msg[type][i] === fn && __msg[type].splice(i, 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
# 斐波那契算法
function fibo(n) {
if (n === 1 || n === 2) {
return 1
}
return fibo(n - 2) + fibo(n - 1)
}
2
3
4
5
6
# 去重
function questStep(arr) {
let key = {}
let value = []
arr.forEach(element => {
if (!(element in key)) {
key[element] = true
value.push(element)
}
})
return value
}
2
3
4
5
6
7
8
9
10
11
12
# 防抖
let antiShake = function () {
let timer = null
return function () {
timer && clearTimeout(timer)
timer = setTimeout(() => {
console.log('防抖成功')
timer = null
}, 2000)
}
}
2
3
4
5
6
7
8
9
10
# 节流
function throttle() {
let timer = null
return () => {
if (!timer) {
timer = setTimeout(() => {
console.log('节流成功')
timer = null
}, 2000)
}
}
}
2
3
4
5
6
7
8
9
10
11
# 其他待整理TODO
# 数据类型判断
typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。
function typeOf(obj) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
2
3
4
5
6
7
8
9
# 继承
# 原型链继承
function Animal() {
this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
return this.colors
}
function Dog() {}
Dog.prototype = new Animal()
let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors) // ['black', 'white', 'brown']
2
3
4
5
6
7
8
9
10
11
12
13
14
原型链继承存在的问题:
- 问题1:原型中包含的引用类型属性将被所有实例共享;
- 问题2:子类在实例化的时候不能给父类构造函数传参;
# 借用构造函数实现继承
function Animal(name) {
this.name = name
this.getName = function() {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal()
2
3
4
5
6
7
8
9
10
11
借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。
# 组合继承
组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2)
// { name: "哈赤", colors: ["black", "white"], age: 1 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 寄生式组合继承
组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。
所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本。
寄生式组合继承写法上和组合继承基本类似,区别是如下这里:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f
2
3
4
5
6
7
8
9
稍微封装下上面添加的代码后:
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
inheritPrototype(Dog, Animal)
2
3
4
5
6
7
8
9
10
11
12
如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ Dog.prototype = Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
2
3
4
5
6
# class 实现继承
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 数组去重
ES5 实现:
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}
2
3
4
5
6
7
ES6 实现:
var unique = arr => [...new Set(arr)]
2
# 数组扁平化
数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:
[1, [2, [3]]].flat(2) // [1, 2, 3]
2
现在就是要实现 flat 这种效果。
ES5 实现:递归。
function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
} else {
result.push(arr[i])
}
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
ES6 实现:
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
2
3
4
5
6
7
# 深浅拷贝
浅拷贝:只考虑对象类型。
function shallowCopy(obj) {
if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
2
3
4
5
6
7
8
9
10
11
12
简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。
function deepClone(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
2
3
4
5
6
7
8
9
10
11
复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 获取当前值的构造函数:获取它的类型
let constructor = target.constructor;
// 检测当前对象target是否与正则、日期格式对象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 创建一个新的特殊对象(正则类/日期类)的实例
return new constructor(target);
}
if (isObject(target)) {
map.set(target, true); // 为循环引用的对象做标记
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
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
# 事件总线(发布订阅模式)
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'
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
# 解析 URL 参数为对象
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 字符串模板
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
2
3
4
5
6
7
8
9
10
测试:
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '布兰',
age: 12
}
render(template, person); // 我是布兰,年龄12,性别undefined
2
3
4
5
6
7
# 图片懒加载
与普通的图片懒加载不同,如下这个多做了 2 个精心处理:
- 图片全部加载完成后移除事件监听;
- 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
const imgLazyLoad = function() {
let count = 0
return function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
}
// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 函数防抖
触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。
简单版:函数内部支持使用 this 和 event 对象;
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
2
3
4
5
6
7
8
9
10
11
12
使用:
var node = document.getElementById('layout')
function getUserAction(e) {
console.log(this, e) // 分别打印:node 这个节点 和 MouseEvent
node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)
2
3
4
5
6
7
最终版:除了支持 this 和 event 外,还支持以下功能:
- 支持立即执行;
- 函数可能有返回值;
- 支持取消功能;
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
} else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
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
使用:
var setUseAction = debounce(getUserAction, 10000, true);
// 使用防抖
node.onmousemove = setUseAction
// 取消防抖
setUseAction.cancel()
2
3
4
5
6
7
参考:JavaScript专题之跟着underscore学防抖 (opens new window)
# 函数节流
触发高频事件,且 N 秒内只执行一次。
简单版:使用时间戳来实现,立即执行一次,然后每 N 秒执行一次。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
最终版:支持取消节流;另外通过传入第三个参数,options.leading 来表示是否可以立即执行一次,opitons.trailing 表示结束调用的时候是否还要执行一次,默认都是 true。 注意设置的时候不能同时将 leading 或 trailing 设置为 false。
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
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
节流的使用就不拿代码举例了,参考防抖的写就行。
参考:JavaScript专题之跟着 underscore 学节流 (opens new window)
# 函数柯里化
什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。还不懂?来举个例子。
function add(a, b, c) {
return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)
2
3
4
5
6
7
现在就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成多次调用每次传一个参数。
function curry(fn) {
let judge = (...args) => {
if (args.length == fn.length) return fn(...args)
return (...arg) => judge(...args, ...arg)
}
return judge
}
2
3
4
5
6
7
8
# 偏函数
什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子:
function add(a, b, c) {
return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
2
3
4
5
6
发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现:
function partial(fn, ...args) {
return (...arg) => {
return fn(...args, ...arg)
}
}
2
3
4
5
6
如上这个功能比较简单,现在我们希望偏函数能和柯里化一样能实现占位功能,比如:
function clg(a, b, c) {
console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3) // 依次打印:1, 2, 3
2
3
4
5
6
_
占的位其实就是 1 的位置。相当于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我们就来写实现:
function partial(fn, ...args) {
return (...arg) => {
args[index] =
return fn(...args, ...arg)
}
}
2
3
4
5
6
7
# JSONP
JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# AJAX
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 实现数组原型方法
# forEach
Array.prototype.forEach2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this) // this 就是当前的数组
const len = O.length >>> 0 // 后面有解释
let k = 0
while (k < len) {
if (k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
参考:forEach#polyfill (opens new window)
O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型。感兴趣可以阅读 something >>> 0是什么意思? (opens new window)。
# map
基于 forEach 的实现能够很容易写出 map 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.map2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ res[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
+ return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# filter
同样,基于 forEach 的实现能够很容易写出 filter 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.filter2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ res.push(O[k])
+ }
}
k++;
}
+ return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# some
同样,基于 forEach 的实现能够很容易写出 some 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.some2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ return true
+ }
}
k++;
}
+ return false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# reduce
Array.prototype.reduce2 = function(callback, initialValue) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0, acc
if (arguments.length > 1) {
acc = initialValue
} else {
// 没传入初始值的时候,取数组中第一个非 empty 的值为初始值
while (k < len && !(k in O)) {
k++
}
if (k > len) {
throw new TypeError( 'Reduce of empty array with no initial value' );
}
acc = O[k++]
}
while (k < len) {
if (k in O) {
acc = callback(acc, O[k], k, O)
}
k++
}
return acc
}
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
# 实现函数原型方法
# call
使用一个指定的 this 值和一个或多个参数来调用一个函数。
实现要点:
- this 可能传入 null;
- 传入不固定个数的参数;
- 函数可能有返回值;
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# apply
apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。
实现要点:
- this 可能传入 null;
- 传入一个数组;
- 函数可能有返回值;
Function.prototype.apply2 = function (context, arr) {
var context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# bind
bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
实现要点:
- bind() 除了 this 外,还可传入多个参数;
- bing 创建的新函数可能传入多个参数;
- 新函数可能被当做构造函数调用;
- 函数可能有返回值;
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 实现 new 关键字
new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。
实现要点:
- new 会产生一个新对象;
- 新对象需要能够访问到构造函数的属性,所以需要重新指定它的原型;
- 构造函数可能会显示返回;
function objectFactory() {
var obj = new Object()
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
var ret = Constructor.apply(obj, arguments);
// ret || obj 这里这么写考虑了构造函数显示返回 null 的情况
return typeof ret === 'object' ? ret || obj : obj;
};
2
3
4
5
6
7
8
9
10
使用:
function person(name, age) {
this.name = name
this.age = age
}
let p = objectFactory(person, '布兰', 12)
console.log(p) // { name: '布兰', age: 12 }
2
3
4
5
6
7
# 实现 instanceof 关键字
instanceof 就是判断构造函数的 prototype 属性是否出现在实例的原型链上。
function instanceOf(left, right) {
let proto = left.__proto__
while (true) {
if (proto === null) return false
if (proto === right.prototype) {
return true
}
proto = proto.__proto__
}
}
2
3
4
5
6
7
8
9
10
11
上面的 left.proto 这种写法可以换成 Object.getPrototypeOf(left)。
# 实现 Object.create
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
Object.create2 = function(proto, propertyObject = undefined) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
function F() {}
F.prototype = proto
const obj = new F()
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
if (proto === null) {
// 创建一个没有原型对象的对象,Object.create(null)
obj.__proto__ = null
}
return obj
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 实现 Object.assign
Object.assign2 = function(target, ...source) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
let ret = Object(target)
source.forEach(function(obj) {
if (obj != null) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key]
}
}
}
})
return ret
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 实现 JSON.stringify
JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space,如果对这两个参数的作用还不了解,建议阅读 MDN (opens new window) 文档。
- 基本数据类型:
- undefined 转换之后仍是 undefined(类型也是 undefined)
- boolean 值转换之后是字符串 "false"/"true"
- number 类型(除了 NaN 和 Infinity)转换之后是字符串类型的数值
- symbol 转换之后是 undefined
- null 转换之后是字符串 "null"
- string 转换之后仍是string
- NaN 和 Infinity 转换之后是字符串 "null"
- 函数类型:转换之后是 undefined
- 如果是对象类型(非函数)
- 如果是一个数组:如果属性值中出现了 undefined、任意的函数以及 symbol,转换成字符串 "null" ;
- 如果是 RegExp 对象:返回 {} (类型是 string);
- 如果是 Date 对象,返回 Date 的 toJSON 字符串值;
- 如果是普通对象;
- 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。
- 如果属性值中出现了 undefined、任意的函数以及 symbol 值,忽略。
- 所有以 symbol 为属性键的属性都会被完全忽略掉。
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
function jsonStringify(data) {
let dataType = typeof data;
if (dataType !== 'object') {
let result = data;
//data 可能是 string/number/null/undefined/boolean
if (Number.isNaN(data) || data === Infinity) {
//NaN 和 Infinity 序列化返回 "null"
result = "null";
} else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
//function 、undefined 、symbol 序列化返回 undefined
return undefined;
} else if (dataType === 'string') {
result = '"' + data + '"';
}
//boolean 返回 String()
return String(result);
} else if (dataType === 'object') {
if (data === null) {
return "null"
} else if (data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON());
} else if (data instanceof Array) {
let result = [];
//如果是数组
//toJSON 方法可以存在于原型链中
data.forEach((item, index) => {
if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
result[index] = "null";
} else {
result[index] = jsonStringify(item);
}
});
result = "[" + result + "]";
return result.replace(/'/g, '"');
} else {
//普通对象
/**
* 循环引用抛错(暂未检测,循环引用时,堆栈溢出)
* symbol key 忽略
* undefined、函数、symbol 为属性值,被忽略
*/
let result = [];
Object.keys(data).forEach((item, index) => {
if (typeof item !== 'symbol') {
//key 如果是symbol对象,忽略
if (data[item] !== undefined && typeof data[item] !== 'function'
&& typeof data[item] !== 'symbol') {
//键值如果是 undefined、函数、symbol 为属性值,忽略
result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
}
}
});
return ("{" + result + "}").replace(/'/g, '"');
}
}
}
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
参考:实现 JSON.stringify (opens new window)
# 实现 JSON.parse
介绍 2 种方法实现:
- eval 实现;
- new Function 实现;
# eval 实现
第一种方式最简单,也最直观,就是直接调用 eval,代码如下:
var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之后得到的对象
2
3
但是直接调用 eval 会存在安全问题,如果数据中可能不是 json 数据,而是可执行的 JavaScript 代码,那很可能会造成 XSS 攻击。因此,在调用 eval 之前,需要对数据进行校验。
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
参考:JSON.parse 三种实现方式 (opens new window)
# new Function 实现
Function 与 eval 有相同的字符串参数特性。
var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();
2
3
# 实现 Promise
实现 Promise 需要完全读懂 Promise A+ 规范 (opens new window),不过从总体的实现上看,有如下几个点需要考虑到:
- then 需要支持链式调用,所以得返回一个新的 Promise;
- 处理异步问题,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 分别把成功和失败的回调存起来;
- 为了让链式调用正常进行下去,需要判断 onFulfilled 和 onRejected 的类型;
- onFulfilled 和 onRejected 需要被异步调用,这里用 setTimeout 模拟异步;
- 处理 Promise 的 resolve;
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (value) = > {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) = > fn());
}
};
let reject = (reason) = > {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) = > fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 解决 onFufilled,onRejected 没有传值的问题
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
// 因为错误的值要让后面访问到,所以这里也要抛出错误,不然会在之后 then 的 resolve 中捕获
onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
throw err;
};
// 每次调用 then 都返回一个新的 promise
let promise2 = new Promise((resolve, reject) = > {
if (this.status === FULFILLED) {
//Promise/A+ 2.2.4 --- setTimeout
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
// x可能是一个proimise
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
//Promise/A+ 2.2.3
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
const resolvePromise = (promise2, x, resolve, reject) = > {
// 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>"));
}
// Promise/A+ 2.3.3.3.3 只能调用一次
let called;
// 后续的条件要严格判断 保证代码能和别的库一起使用
if ((typeof x === "object" && x != null) || typeof x === "function") {
try {
// 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1
let then = x.then;
if (typeof then === "function") {
// 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3
then.call(
x, (y) = > {
// 根据 promise 的状态决定是成功还是失败
if (called) return;
called = true;
// 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
resolvePromise(promise2, y, resolve, reject);
}, (r) = > {
// 只要失败就失败 Promise/A+ 2.3.3.3.2
if (called) return;
called = true;
reject(r);
});
} else {
// 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4
resolve(x);
}
} catch (e) {
// Promise/A+ 2.3.3.2
if (called) return;
called = true;
reject(e);
}
} else {
// 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4
resolve(x);
}
};
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
Promise 写完之后可以通过 promises-aplus-tests 这个包对我们写的代码进行测试,看是否符合 A+ 规范。不过测试前还得加一段代码:
// promise.js
// 这里是上面写的 Promise 全部代码
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;
2
3
4
5
6
7
8
9
10
11
12
13
全局安装:
npm i promises-aplus-tests -g
2
终端下执行验证命令:
promises-aplus-tests promise.js
2
上面写的代码可以顺利通过全部 872 个测试用例。
参考:
# Promise.resolve
Promsie.resolve(value) 可以将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。
Promise.resolve = function(value) {
// 如果是 Promsie,则直接输出它
if(value instanceof Promise){
return value
}
return new Promise(resolve => resolve(value))
}
2
3
4
5
6
7
8
参考:深入理解 Promise (opens new window)
# Promise.reject
和 Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason))
}
2
3
4
# Promise.all
Promise.all 的规则是这样的:
- 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;
- 只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;
- 只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;
Promise.all = function(promiseArr) {
let index = 0, result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
index++
result[i] = val
if (index === promiseArr.length) {
resolve(result)
}
}, err => {
reject(err)
})
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Promise.race
Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
rejecte(err)
})
})
})
}
2
3
4
5
6
7
8
9
10
11
12
# Promise.allSettled
Promise.allSettled 的规则是这样:
- 所有 Promise 的状态都变化了,那么新返回一个状态是 fulfilled 的 Promise,且它的值是一个数组,数组的每项由所有 Promise 的值和状态组成的对象;
- 如果有一个是 pending 的 Promise,则返回一个状态是 pending 的新实例;
Promise.allSettled = function(promiseArr) {
let result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
result.push({
status: 'fulfilled',
value: val
})
if (result.length === promiseArr.length) {
resolve(result)
}
}, err => {
result.push({
status: 'rejected',
reason: err
})
if (result.length === promiseArr.length) {
resolve(result)
}
})
})
})
}
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
# Promise.any
Promise.any 的规则是这样:
- 空数组或者所有 Promise 都是 rejected,则返回状态是 rejected 的新 Promsie,且值为 AggregateError 的错误;
- 只要有一个是 fulfilled 状态的,则返回第一个是 fulfilled 的新实例;
- 其他情况都会返回一个 pending 的新实例;
Promise.any = function(promiseArr) {
let index = 0
return new Promise((resolve, reject) => {
if (promiseArr.length === 0) return
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
index++
if (index === promiseArr.length) {
reject(new AggregateError('All promises were rejected'))
}
})
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 01.数组扁平化
数组扁平化是指将一个多维数组变为一个一维数组
const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]
2
3
# 方法一:使用flat()
const res1 = arr.flat(Infinity);
2
# 方法二:利用正则
const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');
2
但数据类型都会变为字符串
# 方法三:正则改良版本
const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');
2
# 方法四:使用reduce
const flatten = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
const res4 = flatten(arr);
2
3
4
5
6
7
# 方法五:函数递归
const res5 = [];
const fn = arr => {
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
fn(arr[i]);
} else {
res5.push(arr[i]);
}
}
}
fn(arr);
2
3
4
5
6
7
8
9
10
11
12
# 02.数组去重
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
2
3
# 方法一:利用Set
const res1 = Array.from(new Set(arr));
2
# 方法二:两层for循环+splice
const unique1 = arr => {
let len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
// 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
len--;
j--;
}
}
}
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 方法三:利用indexOf
const unique2 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
}
2
3
4
5
6
7
8
当然也可以用include、filter,思路大同小异。
# 方法四:利用include
const unique3 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
2
3
4
5
6
7
8
# 方法五:利用filter
const unique4 = arr => {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
2
3
4
5
6
# 方法六:利用Map
const unique5 = arr => {
const map = new Map();
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!map.has(arr[i])) {
map.set(arr[i], true)
res.push(arr[i]);
}
}
return res;
}
2
3
4
5
6
7
8
9
10
11
12
# 03.类数组转化为数组
类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。
# 方法一:Array.from
Array.from(document.querySelectorAll('div'))
2
# 方法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
2
# 方法三:扩展运算符
[...document.querySelectorAll('div')]
2
# 方法四:利用concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
2
# 04.Array.prototype.filter()
Array.prototype.filter = function(callback, thisArg) {
if (this == undefined) {
throw new TypeError('this is null or not undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + 'is not a function');
}
const res = [];
// 让O成为回调函数的对象传递(强制转换对象)
const O = Object(this);
// >>>0 保证len为number,且为正整数
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {
// 检查i是否在O的属性(会检查原型链)
if (i in O) {
// 回调函数调用传参
if (callback.call(thisArg, O[i], i, O)) {
res.push(O[i]);
}
}
}
return res;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对于>>>0
有疑问的:解释>>>0的作用 (opens new window)
# 05.Array.prototype.map()
Array.prototype.map = function(callback, thisArg) {
if (this == undefined) {
throw new TypeError('this is null or not defined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const res = [];
// 同理
const O = Object(this);
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {
if (i in O) {
// 调用回调函数并传入新数组
res[i] = callback.call(thisArg, O[i], i, this);
}
}
return res;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 06.Array.prototype.forEach()
forEach
跟map类似,唯一不同的是forEach
是没有返回值的。
Array.prototype.forEach = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined');
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
while (k < len) {
if (k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 07.Array.prototype.reduce()
Array.prototype.reduce = function(callback, initialValue) {
if (this == undefined) {
throw new TypeError('this is null or not defined');
}
if (typeof callback !== 'function') {
throw new TypeError(callbackfn + ' is not a function');
}
const O = Object(this);
const len = this.length >>> 0;
let accumulator = initialValue;
let k = 0;
// 如果第二个参数为undefined的情况下
// 则数组的第一个有效值作为累加器的初始值
if (accumulator === undefined) {
while (k < len && !(k in O)) {
k++;
}
// 如果超出数组界限还没有找到累加器的初始值,则TypeError
if (k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = O[k++];
}
while (k < len) {
if (k in O) {
accumulator = callback.call(undefined, accumulator, O[k], k, O);
}
k++;
}
return accumulator;
}
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
# 08.Function.prototype.apply()
第一个参数是绑定的this,默认为window
,第二个参数是数组或类数组
Function.prototype.apply = function(context = window, args) {
if (typeof this !== 'function') {
throw new TypeError('Type Error');
}
const fn = Symbol('fn');
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
2
3
4
5
6
7
8
9
10
11
12
# 09.Function.prototype.call
于call
唯一不同的是,call()
方法接受的是一个参数列表
Function.prototype.call = function(context = window, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Type Error');
}
const fn = Symbol('fn');
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
2
3
4
5
6
7
8
9
10
11
12
# 10.Function.prototype.bind
Function.prototype.bind = function(context, ...args) {
if (typeof this !== 'function') {
throw new Error("Type Error");
}
// 保存this的值
var self = this;
return function F() {
// 考虑new的情况
if(this instanceof F) {
return new self(...args, ...arguments)
}
return self.apply(context, [...args, ...arguments])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 11.debounce(防抖)
触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。
const debounce = (fn, time) => {
let timeout = null;
return function() {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, time);
}
};
2
3
4
5
6
7
8
9
10
防抖常应用于用户进行搜索输入节约请求资源,window
触发resize
事件时进行防抖只触发一次。
# 12.throttle(节流)
高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。
const throttle = (fn, time) => {
let flag = true;
return function() {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, arguments);
flag = true;
}, time);
}
}
2
3
4
5
6
7
8
9
10
11
12
节流常应用于鼠标不断点击触发、监听滚动事件。
# 13.函数珂里化
指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用,例如f(1)(2)
经典面试题:实现add(1)(2)(3)(4)=10;
、 add(1)(1,2,3)(2)=9;
function add() {
const _args = [...arguments];
function fn() {
_args.push(...arguments);
return fn;
}
fn.toString = function() {
return _args.reduce((sum, cur) => sum + cur);
}
return fn;
}
2
3
4
5
6
7
8
9
10
11
12
# 14.模拟new操作
3个步骤:
- 以
ctor.prototype
为原型创建一个对象。 - 执行构造函数并将this绑定到新创建的对象上。
- 判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。
function newOperator(ctor, ...args) {
if (typeof ctor !== 'function') {
throw new TypeError('Type Error');
}
const obj = Object.create(ctor.prototype);
const res = ctor.apply(obj, args);
const isObject = typeof res === 'object' && res !== null;
const isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
}
2
3
4
5
6
7
8
9
10
11
12
# 15.instanceof
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
const myInstanceof = (left, right) => {
// 基本数据类型都返回false
if (typeof left !== 'object' || left === null) return false;
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto === null) return false;
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
2
3
4
5
6
7
8
9
10
11
# 16.原型继承
这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.type = 'children';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
2
3
4
5
6
7
8
9
10
# 17.Object.is
Object.is
解决的主要是这两个问题:
+0 === -0 // true
NaN === NaN // false
const is= (x, y) => {
if (x === y) {
// +0和-0应该不相等
return x !== 0 || y !== 0 || 1/x === 1/y;
} else {
return x !== x && y !== y;
}
}
2
3
4
5
6
7
8
9
10
11
12
# 18.Object.assign
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象(请注意这个操作是浅拷贝)
Object.defineProperty(Object, 'assign', {
value: function(target, ...args) {
if (target == null) {
return new TypeError('Cannot convert undefined or null to object');
}
// 目标对象需要统一是引用数据类型,若不是会自动转换
const to = Object(target);
for (let i = 0; i < args.length; i++) {
// 每一个源对象
const nextSource = args[i];
if (nextSource !== null) {
// 使用for...in和hasOwnProperty双重判断,确保只拿到本身的属性、方法(不包含继承的)
for (const nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
// 不可枚举
enumerable: false,
writable: true,
configurable: true,
})
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
# 19.深拷贝
递归的完整版本(考虑到了Symbol属性):
const cloneDeep1 = (target, hash = new WeakMap()) => {
// 对于传入参数处理
if (typeof target !== 'object' || target === null) {
return target;
}
// 哈希表中存在直接返回
if (hash.has(target)) return hash.get(target);
const cloneTarget = Array.isArray(target) ? [] : {};
hash.set(target, cloneTarget);
// 针对Symbol属性
const symKeys = Object.getOwnPropertySymbols(target);
if (symKeys.length) {
symKeys.forEach(symKey => {
if (typeof target[symKey] === 'object' && target[symKey] !== null) {
cloneTarget[symKey] = cloneDeep1(target[symKey]);
} else {
cloneTarget[symKey] = target[symKey];
}
})
}
for (const i in target) {
if (Object.prototype.hasOwnProperty.call(target, i)) {
cloneTarget[i] =
typeof target[i] === 'object' && target[i] !== null
? cloneDeep1(target[i], hash)
: target[i];
}
}
return cloneTarget;
}
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
# 20.Promise
实现思路:Promise源码实现 (opens new window)
// 模拟实现Promise
// Promise利用三大手段解决回调地狱:
// 1. 回调函数延迟绑定
// 2. 返回值穿透
// 3. 错误冒泡
// 定义三种状态
const PENDING = 'PENDING'; // 进行中
const FULFILLED = 'FULFILLED'; // 已成功
const REJECTED = 'REJECTED'; // 已失败
class Promise {
constructor(exector) {
// 初始化状态
this.status = PENDING;
// 将成功、失败结果放在this上,便于then、catch访问
this.value = undefined;
this.reason = undefined;
// 成功态回调函数队列
this.onFulfilledCallbacks = [];
// 失败态回调函数队列
this.onRejectedCallbacks = [];
const resolve = value => {
// 只有进行中状态才能更改状态
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 成功态函数依次执行
this.onFulfilledCallbacks.forEach(fn => fn(this.value));
}
}
const reject = reason => {
// 只有进行中状态才能更改状态
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 失败态函数依次执行
this.onRejectedCallbacks.forEach(fn => fn(this.reason))
}
}
try {
// 立即执行executor
// 把内部的resolve和reject传入executor,用户可调用resolve和reject
exector(resolve, reject);
} catch(e) {
// executor执行出错,将错误内容reject抛出去
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'? onRejected :
reason => { throw new Error(reason instanceof Error ? reason.message : reason) }
// 保存this
const self = this;
return new Promise((resolve, reject) => {
if (self.status === PENDING) {
self.onFulfilledCallbacks.push(() => {
// try捕获错误
try {
// 模拟微任务
setTimeout(() => {
const result = onFulfilled(self.value);
// 分两种情况:
// 1. 回调函数返回值是Promise,执行then操作
// 2. 如果不是Promise,调用新Promise的resolve函数
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {
reject(e);
}
});
self.onRejectedCallbacks.push(() => {
// 以下同理
try {
setTimeout(() => {
const result = onRejected(self.reason);
// 不同点:此时是reject
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {
reject(e);
}
})
} else if (self.status === FULFILLED) {
try {
setTimeout(() => {
const result = onFulfilled(self.value);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
});
} catch(e) {
reject(e);
}
} else if (self.status === REJECTED) {
try {
setTimeout(() => {
const result = onRejected(self.reason);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {
reject(e);
}
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof Promise) {
// 如果是Promise实例,直接返回
return value;
} else {
// 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
return new Promise((resolve, reject) => resolve(value));
}
}
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
static all(promiseArr) {
const len = promiseArr.length;
const values = new Array(len);
// 记录已经成功执行的promise个数
let count = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < len; i++) {
// Promise.resolve()处理,确保每一个都是promise实例
Promise.resolve(promiseArr[i]).then(
val => {
values[i] = val;
count++;
// 如果全部执行完,返回promise的状态就可以改变了
if (count === len) resolve(values);
},
err => reject(err),
);
}
})
}
static race(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(
val => resolve(val),
err => reject(err),
)
})
})
}
}
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
# 21.Promise.all
Promise.all
是支持链式调用的,本质上就是返回了一个Promise实例,通过resolve
和reject
来改变实例状态。
Promise.myAll = function(promiseArr) {
return new Promise((resolve, reject) => {
const ans = [];
let index = 0;
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i]
.then(res => {
ans[i] = res;
index++;
if (index === promiseArr.length) {
resolve(ans);
}
})
.catch(err => reject(err));
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 22.Promise.race
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
// 如果不是Promise实例需要转化为Promise实例
Promise.resolve(p).then(
val => resolve(val),
err => reject(err),
)
})
})
}
2
3
4
5
6
7
8
9
10
11
12
# 23.Promise并行限制
就是实现有并行限制的Promise调度器问题。
详细实现思路:某条高频面试原题:实现有并行限制的Promise调度器 (opens new window)
class Scheduler {
constructor() {
this.queue = [];
this.maxCount = 2;
this.runCounts = 0;
}
add(promiseCreator) {
this.queue.push(promiseCreator);
}
taskStart() {
for (let i = 0; i < this.maxCount; i++) {
this.request();
}
}
request() {
if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
return;
}
this.runCounts++;
this.queue.shift()().then(() => {
this.runCounts--;
this.request();
});
}
}
const timeout = time => new Promise(resolve => {
setTimeout(resolve, time);
})
const scheduler = new Scheduler();
const addTask = (time,order) => {
scheduler.add(() => timeout(time).then(()=>console.log(order)))
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4
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
# 24.JSONP
script标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于GET请求
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = '';
for (let key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
dataSrc += `${key}=${params[key]}&`;
}
}
dataSrc += `callback=${callbackName}`;
return `${url}?${dataSrc}`;
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script');
scriptEle.src = generateUrl();
document.body.appendChild(scriptEle);
window[callbackName] = data => {
resolve(data);
document.removeChild(scriptEle);
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 25.AJAX
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 26.event模块
实现node中回调函数的机制,node中回调函数其实是内部使用了观察者模式。
观察者模式:定义了对象间一种一对多的依赖关系,当目标对象Subject发生改变时,所有依赖它的对象Observer都会得到通知。
function EventEmitter() {
this.events = new Map();
}
// 需要实现的一些方法:
// addListener、removeListener、once、removeAllListeners、emit
// 模拟实现addlistener方法
const wrapCallback = (fn, once = false) => ({ callback: fn, once });
EventEmitter.prototype.addListener = function(type, fn, once = false) {
const hanlder = this.events.get(type);
if (!hanlder) {
// 没有type绑定事件
this.events.set(type, wrapCallback(fn, once));
} else if (hanlder && typeof hanlder.callback === 'function') {
// 目前type事件只有一个回调
this.events.set(type, [hanlder, wrapCallback(fn, once)]);
} else {
// 目前type事件数>=2
hanlder.push(wrapCallback(fn, once));
}
}
// 模拟实现removeListener
EventEmitter.prototype.removeListener = function(type, listener) {
const hanlder = this.events.get(type);
if (!hanlder) return;
if (!Array.isArray(this.events)) {
if (hanlder.callback === listener.callback) this.events.delete(type);
else return;
}
for (let i = 0; i < hanlder.length; i++) {
const item = hanlder[i];
if (item.callback === listener.callback) {
hanlder.splice(i, 1);
i--;
if (hanlder.length === 1) {
this.events.set(type, hanlder[0]);
}
}
}
}
// 模拟实现once方法
EventEmitter.prototype.once = function(type, listener) {
this.addListener(type, listener, true);
}
// 模拟实现emit方法
EventEmitter.prototype.emit = function(type, ...args) {
const hanlder = this.events.get(type);
if (!hanlder) return;
if (Array.isArray(hanlder)) {
hanlder.forEach(item => {
item.callback.apply(this, args);
if (item.once) {
this.removeListener(type, item);
}
})
} else {
hanlder.callback.apply(this, args);
if (hanlder.once) {
this.events.delete(type);
}
}
return true;
}
EventEmitter.prototype.removeAllListeners = function(type) {
const hanlder = this.events.get(type);
if (!hanlder) return;
this.events.delete(type);
}
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
# 27.图片懒加载
可以给img标签统一自定义属性data-src='default.png'
,当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。
function lazyload() {
const imgs = document.getElementsByTagName('img');
const len = imgs.length;
// 视口的高度
const viewHeight = document.documentElement.clientHeight;
// 滚动条高度
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
for (let i = 0; i < len; i++) {
const offsetHeight = imgs[i].offsetTop;
if (offsetHeight < viewHeight + scrollHeight) {
const src = imgs[i].dataset.src;
imgs[i].src = src;
}
}
}
// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 28.滚动加载
原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。
window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
if (clientHeight + scrollTop >= scrollHeight) {
// 检测到滚动至页面底部,进行后续操作
// ...
}
}, false);
2
3
4
5
6
7
8
9
10
一个Demo:页面滚动加载的Demo (opens new window)
# 29.渲染几万条数据不卡住页面
渲染大数据时,合理使用createDocumentFragment和requestAnimationFrame,将操作切分为一小段一小段执行。
setTimeout(() => {
// 插入十万条数据
const total = 100000;
// 一次插入的数据
const once = 20;
// 插入数据需要的次数
const loopCount = Math.ceil(total / once);
let countOfRender = 0;
const ul = document.querySelector('ul');
// 添加数据的方法
function add() {
const fragment = document.createDocumentFragment();
for(let i = 0; i < once; i++) {
const li = document.createElement('li');
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if(countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0)
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.打印出当前网页使用了多少种HTML元素
一行代码可以解决:
const fn = () => {
return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;
}
2
3
4
值得注意的是:DOM操作返回的是类数组,需要转换为数组之后才可以调用数组的方法。
# 31.将VirtualDom转化为真实DOM结构
这是当前SPA应用的核心概念之一
// vnode结构:
// {
// tag,
// attrs,
// children,
// }
//Virtual DOM => DOM
function render(vnode, container) {
container.appendChild(_render(vnode));
}
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === 'number') {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach(key => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
})
}
// 子数组进行递归操作
vnode.children.forEach(child => render(child, dom));
return dom;
}
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
# 32.字符串解析问题
var a = {
b: 123,
c: '456',
e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'
2
3
4
5
6
7
8
实现函数使得将str字符串中的{}
内的变量替换,如果属性不存在保持原样(比如{a.d}
)
类似于模版字符串,但有一点出入,实际上原理大差不差
const fn1 = (str, obj) => {
let res = '';
// 标志位,标志前面是否有{
let flag = false;
let start;
for (let i = 0; i < str.length; i++) {
if (str[i] === '{') {
flag = true;
start = i + 1;
continue;
}
if (!flag) res += str[i];
else {
if (str[i] === '}') {
flag = false;
res += match(str.slice(start, i), obj);
}
}
}
return res;
}
// 对象匹配操作
const match = (str, obj) => {
const keys = str.split('.').slice(1);
let index = 0;
let o = obj;
while (index < keys.length) {
const key = keys[index];
if (!o[key]) {
return `{${str}}`;
} else {
o = o[key];
}
index++;
}
return o;
}
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
# 参考链接
待看https://juejin.cn/post/6844903856489365518
待看Promise/async/Generator实现原理解析 (opens new window)
https://juejin.cn/post/6946022649768181774
https://juejin.cn/post/6875152247714480136