手写js轮子

本文最后更新于:2021年11月11日 上午

为了更好地理解一些js方法的原理,决定自己捏捏轮子

call,apply,bind

call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myCall = function(...args){
const newThis = args[0] || window;
const realArgs = args.slice(1);
const func = this;

const funcSym = Symbol('func'); // 创建临时函数
newThis[funcSym] = func;

const res = newThis[funcSym](...realArgs); // 执行

delete newThis[funcSym]; // 删除

return res; // 返回执行值
}

apply()

apply()call()之间除了接收的参数不同,可以说没有其他区别。因此大致的实现是和call()一样的,除了在处理接收到参数方面点区别,这里就不贴了。

bind()

bind()是ES5引入的函数。bind()call()的区别在于它不会立即执行函数,而是返回一个修改了this的新函数,执行需要调用该新函数。

前面已经实现了call(), 这里就直接借用call()来实现了。

1
2
3
4
5
6
7
8
9
Function.prototype.myBind = function(...args){

const newThis = args[0];
const realArgs = args.slice(1);
const func = this;
return function(...newArgs){
return func.call(newThis, ...realArgs, ...newArgs)
}
}

Promise

Promise极简

1.实现了基本的异步、回调功能。(then和catch)

2.未实现链式调用,如.then().catch()


Promise()的参数是一个带有resolvereject两个参数的函数,据此我们可以先抽象出MyPromise的结构

1
2
3
4
5
6
7
8
9
10
function MyPromise(executor) {
this.status = "pending"

// 把resolve()和reject()绑定到当前对象,也就是令二者的this值指向MyPromise的实例
executor(resolve.bind(this), reject.bind(this))

function resolve(params) {}
function reject(error) {}
}
MyPromise.prototype.then = function (resolve, reject) {}

实现resolve(),rejected()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
function MyPromise(executor) {
this.status = "pending"
this.successCallback = undefined // 'fulfilled'时调用的函数,在then()中赋值
this.failCallback = undefined // 'rejected'时调用的函数,在then()/catch()中赋值

executor(resolve.bind(this), reject.bind(this))

function resolve(params) {
if (this.status === "pending"){
setTimeout(() => { // 使用setTimeout的宏任务队列模拟微任务队列
this.status = "fulfilled"
this.successCallback(params)
}, 0);
}
}
function reject(error) {
if(this.status === "pending"){
setTimeout(() => { // 同上
this.status = "rejected"
this.failCallback(error)
}, 0);
}
}
}

MyPromise.prototype.then = function (resolve, reject) {
this.successCallback = resolve
this.failCallback = reject
}

MyPromise.prototype.catch = function (reject) {
this.failCallback = reject
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
/* then */
const p4 = new MyPromise((resolve, reject) => {
console.log(1)
resolve(2)
console.log(3)
}).then(v => console.log(v)) // 输出 1 3 2


/* catch */
const p1 = new MyPromise((resolve, reject) => {
reject("err-1")
}).catch(err=> console.log("err:", err)) // 输出 err: err-1

Promise.any()

虽然说我们无法使用.then().catch()这种链式调用来捕获错误,但实际上then()本身就可以捕获错误了(利用第二个参数reject),因此实现MyPromise.any()这里我就只使用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
/* ES2021 引入any()概念 */ 
MyPromise.any = function (list) {
return new MyPromise((resolve, reject) => {
let isFulfilled = false
let len = 0
let AggregateError = [] // 收集错误
for (i of list) {
i.then( // then(resolve, reject)
(value) => {
if (!isFulfilled) {
isFulfilled = true
resolve(value) // 以第一个'fulfilled'的Promise实例作为返回值
}
},
(error) => {
len++
AggregateError.push(error)
if (len === list.length) { //所有的promise都reject了
reject(AggregateError)
}
}
)
}
})
}

测试

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
/* 存在fulfilled的Promise实例 */
const p1 = new MyPromise((resolve, reject) => {
reject("err-1")
})
const p2 = new MyPromise((resolve, reject) => {
reject("err-2")
})
const p3 = new MyPromise((resolve, reject) => {
resolve(3)
})
let p = new MyPromise.any([p1, p2, p3]).then(
(v) => {
console.log("any:", v)
},
(err) => {
console.log("any-err", err)
}
)
// 输出:any: 3

/* Primise实例全部rejected */
const p1 = new MyPromise((resolve, reject) => {
reject("err-1")
})
const p2 = new MyPromise((resolve, reject) => {
reject("err-2")
})
const p3 = new MyPromise((resolve, reject) => {
reject("err-3")
})
let p = new MyPromise.any([p1, p2, p3]).then(
(v) => {
console.log("any:", v)
},
(err) => {
console.log("any-err", err)
}
)
// 输出:any-err [ 'err-1', 'err-2', 'err-3' ]

还有一个神奇的实现思路是利用Promise.all()。把所有Promise实例(包括Promise.all()本身)的resolvereject参数位置交换一下,这样就可以把all()转换成any()了。这是利用了和any()all()二者存在类似于的关系(数电学过吧),因此把all()的逻辑取反就得到了any(),代码如下(这个是网上抄的,我找不到原文了)

1
2
3
4
5
6
7
function reverse (promise) {
return new Promise(
(resolve, reject) => Promise.resolve(promise).then(reject, resolve))
}

MyPromise.any = (iterable) =>
reverse(Promise.all([...iterable].map(reverse)))

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MyPromise.any([
Promise.reject('✗'),
Promise.reject('✗'),
Promise.resolve('✓'),
]).then(function(value) {
console.log(value)
// value is '✓'
});

MyPromise.any([
Promise.reject('✗'),
Promise.reject('✗'),
]).catch(function(reasons) {
console.log(reasons)
// reasons is ['✗', '✗']
});

ajax

相关函数/属性

  • XMLHttpRequest.open( method, url, async ) – 初始化一个请求。详见

    async:是否异步

  • XMLHttpRequest.send() – 发送 HTTP 请求。详见

    异步请求(默认):会在请求发送后立即返回;

    同步请求:直到响应到达后才会返回。

  • XMLHttpRequest.onreadystatechange –只要 readyState 值发生变化,就会执行

  • XMLHttpRequest.readyState – XMLHttpRequest 的状态码

    状态 说明
    0 UNSENT 代理被创建,但尚未调用 open() 方法。
    1 OPENED open() 方法已经被调用。
    2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。
    3 LOADING 下载中; responseText 属性已经包含部分数据。
    4 DONE 下载操作已完成。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let xhr = null
xhr = new XMLHttpRequest();
if (xhr != null) {
xhr.onreadystatechange = stateChange
xhr.open('GET', 'http://127.0.0.1:3000/', false)
xhr.send()
}
function stateChange() {
if (xhr.readyState === 4) {
if (xhr.status === 200) { // 不需要考虑304,见下文
console.log(xhr.responseText) // 响应数据
alert('success')
}
else {
console.log(xhr.status)
alert("something error")
}
}
}

请求结果判断200或者0状态就可以了,跳转不需要判断的,因为返回的是最后跳转的网址的状态

另外,ie6以前没有XMLHttpRequest….so,改用这个

1
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");

深拷贝

预处理对象obj1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj1 = {
id: '1',
name: 'litstronger',
hobbies: null,
childObj: {
test: "deepClone",
},
childArray: [1, 2, 3],
regExp: /a/,
func: function () {
console.log(1)
},
arrowFunc: () => {
console.log('arrow func')
}
}

利用JSON处理

使用JSON来深拷贝应该是比较实用的,简单且能满足大多数场景下的需求。缺点是无法处理函数正则这两种对象。

1
console.log(JSON.parse(JSON.stringify(obj1)))

output:

1
2
3
4
5
6
7
8
{
id: 1,
name: 'litstronger',
hobbies: null,
childObj: { test: 'deepClone' },
childArray: [ 1, 2, 3 ],
regExp: {}
}

可以看到处理后正则变空,函数直接整个没了。

手写递归处理

1.简陋结构

1
2
3
4
5
6
7
8
9
10
11
12
function deepClone(target){
if(target instanceof Object){
let tempObj = {}
for(let key in target){
tempObj[key] = deepClone(target[key])
}
return tempObj
}
else{
return target
}
}

output:

1
2
3
4
5
6
7
8
9
10
{
id: '1',
name: 'litstronger',
hobbies: null,
childObj: { test: 'deepClone' },
childArray: { '0': 1, '1': 2, '2': 3 },
regExp: {},
func: {},
arrowFunc: {}
}

显然,这里只能处理最基本的对象{},数组的话需要再做处理。

2.处理数组

也不难,因为基本对象和数组都可以用中括号[]进行属性访问,所以在处理上是一样的。只需要在创建tempObj时加个判断

1
let tempObj = Array.isArray(target)?[]:{}

如下

1
2
3
4
5
6
7
8
9
10
11
12
function deepClone(target){
if(target instanceof Object){
let tempObj = Array.isArray(target)?[]:{}
for(let key in target){
tempObj[key] = deepClone(target[key])
}
return tempObj
}
else{
return target
}
}

output:

1
2
3
4
5
6
7
8
9
10
{
id: '1',
name: 'litstronger',
hobbies: null,
childObj: { test: 'deepClone' },
childArray: [ 1, 2, 3 ],
regExp: {},
func: {},
arrowFunc: {}
}

可见,数组拷贝也正常了

3.循环引用

咕。 (咕了的终究还是要还的)

循环引用就是对象的引用出现了循环,比如

1
2
obj1.childObj2 = obj1 
deepClone(obj1) // RangeError: Maxmum call stack size exceeded

解决的思路类似于去重,可以建一个Map来记录已经处理过的属性,以原对象的属性作为键(target),以拷贝的内容作为值(tempObj)

如果Map中已经有记录了就直接返回记录值,否则新增键值对。如下

1
2
3
4
5
m = new Map()
if (m.has(target)) {
return m.get(target)
}
m.set(target, tempObj)

由于每次递归调用中我们都需要用到Map的最新内容,考虑把它作为函数参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deepClone(target, m = new Map()) {
if (target instanceof Object) {
let tempObj = Array.isArray(target) ? [] : {}

// 解决循环引用
if (m.has(target)) {
return m.get(target)
}
m.set(target, tempObj)

for (let key in target) {
tempObj[key] = deepClone(target[key], m)
}
return tempObj
}
else {
return target
}
}
obj1.childObj2 = obj1
obj1.childObj.childObj2 = obj1
console.log(deepClone(obj1))

output

1
2
3
4
5
6
7
8
9
10
11
<ref *1> {
id: '1',
name: 'litstronger',
hobbies: null,
childObj: { test: 'deepClone', childObj2: [Circular *1] },
childArray: [ 1, 2, 3 ],
regExp: {},
func: {},
arrowFunc: {},
childObj2: [Circular *1]
}

4.处理函数和正则

啊。这两个不处理心里别扭啊。(别扭就别扭吧,反正没啥用orz

数组

数组需要考虑两种情况:普通函数、箭头函数。

区分:可以利用箭头函数没有prototype的特点

拷贝

  • 普通函数:

    bind()可以返回一个函数的拷贝啊。MDN对bind()的返回值是这么说的。我试着实现了下拷贝,应该没问题

    A copy of the given function with the specified this value, and initial arguments (if provided).

  • 箭头函数:

    利用toString()和eval()

综上,对函数的处理如下

1
2
3
4
5
6
7
8
if (typeOfValue('Function')(target)) {
if (target.prototype){ // 区别箭头函数
return target.bind(null)
}
else {
return eval(target.toString())
}
}

这个我想了蛮久了,网上找到的方法似乎都挺麻烦的,比如用正则匹配参数和函数体。

lodash库的深拷贝对函数的处理也是直接返回。可能拷贝函数真没什么应用场景吧。

截取代码如下

1
2
3
4
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}

正则

使用RegExp()新创建一个正则需要两个参数,分别是主体内容部分(source)标志(flags),比如在/a/i

1
2
3
4
5
let reg = /a/i
console.log(reg.source, reg.flags) // a i

let reg2 = RegExp(reg.source, reg.flags) // 拷贝
console.log(reg === reg2) // false

至于正则内部的其他属性,还是需要遍历拷贝,使用前文的拷贝方式就可以了。

5.判断类型

用于区分基本对象、数组、函数和正则

1
let typeOfValue = type => v => `[object ${type}]` === Object.prototype.toString.call(v)

6.合并

把上述代码块整合起来(除了循环引用部分),看看结果吧

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
let obj1 = {
id: '1',
name: 'litstronger',
age: '18',
childObj: {
test: "deepClone",
},
childArray: [1, 2, 3],
regExp: /a/,
func: function () {
console.log(1)
},
arrowFunc: () => {
console.log('arrow func')
}
}

let typeOfValue = type => v => `[object ${type}]` === Object.prototype.toString.call(v)

function deepClone(target) {
if (target instanceof Object) {
let tempObj
if (typeOfValue('RegExp')(target)) { // 正则
tempObj = new RegExp(target.source, target.flags)
}
else if (typeOfValue('Function')(target)) { // 函数
if (target.prototype)
return target.bind(null)
else {
return eval(target.toString())
}
}
else { // [] and {}
tempObj = Array.isArray(target) ? [] : {}
}

for(let key in target){
tempObj[key] = deepClone(target[key])
}
return tempObj
}
else {
return target
}
}
console.log(obj2)
console.log("正则", obj1.regExp === obj2.regExp)
console.log("普通函数", obj1.func === obj2.func)
console.log("箭头函数", obj1.arrowFunc === obj2.arrowFunc)

output:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
id: '1',
name: 'litstronger',
age: '18',
childObj: { test: 'deepClone' },
childArray: [ 1, 2, 3 ],
regExp: /a/,
func: [Function: bound func],
arrowFunc: [Function (anonymous)]
}
正则 false
普通函数 false
箭头函数 false

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!