前言

前端开发中原生JS的理解一定要足够透彻,这篇文章主要介绍面试过程中可能会出现的实现JS中的关键字或一些特殊方法的原理题。由于经常看了就忘,于是在此做下记录。

实现new关键字

要实现new方法的原理,首先要知道new操作符具体干了什么

  • 在js中,new操作符用于创建一个给定构造函数的实例对象。
1
2
3
4
5
6
7
8
9
10
11
// 定义构造函数Person
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

注:如果构造函数中存在返回值

  1. 返回值为基本数据类型,那么new创建的实例对象不会受返回值的结果影响。
  2. 返回值为引用数据类型(对象、数组),那么new创建的实例对象就是返回的数据结果。如下所示
1
2
3
4
5
6
7
8
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'
  • new关键字实现创建对象的步骤:

    1. 创建一个空的实例对象 => obj
    2. 将实例对象与构造函数通过原型连接起来 => obj.__proto__ === 构造函数.prototype
    3. 将构造函数中的this绑定到实例对象obj上 => 构造函数.apply(obj, 参数)
    4. 判断构造函数的返回值类型;如果返回引用类型那么空对象赋值为构造函数返回结果,否则返回该对象。
  • 实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 实现new的功能
* @param {Function} Func 构造函数
* @params args 构造函数的参数
*/
function myNew(Func, ...args){
const obj = {}
obj.__proto__ = Func.prototype
let result = Func.apply(obj, args)
return typeof result === 'object' ? result : obj
}
  • 测试函数的功能
1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () {
console.log("Hi, I'm", this.name)
}
const lisi = myNew(Person, '李四', 100)
console.log(lisi) // Person {name: "李四", age: 100}
lisi.sayHi() // Hi, I'm 李四

callapplybind的实现

先说明以下三者的用途和区别

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
  • bind执行后是返回绑定this之后的函数,apply、call 则是立即执行

使用方式如下:

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
function fn(...args) {
console.log('fn函数的this', this)
console.log('fn函数的参数', args)
}

console.log('------------fn------------')
fn(1, 2, 3)
// 1、使用apply改变this指向,第二个参数必须是数组。
// func.apply(thisArg, [argsArray])
// fn.apply(obj, 1, 2, 3) // 这样使用apply会报以下错误:CreateListFromArrayLike called on non-object
console.log('------------fn.apply------------')
fn.apply(obj, [1, 2, 3])
// 2、使用call改变this指向,后面的参数为参数列表,而不是数组,传递数组的话,只会给第一个参数赋值
// function.call(thisArg, arg1, arg2, ...)
console.log('------------fn.call------------')
fn.call(obj, 1, 2, 3)
// 3、使用bind改变this指向
// function.bind(thisArg[, arg1[, arg2[, ...]]])
console.log('------------fn.bind------------')
fn.bind(obj, 1, 2, 3) // bind改变this不会立即执行,控制台并没有输出
// bind会有一个返回值,返回一个原函数的拷贝,并拥有指定的 this 值和初始参数
console.log('------------fn.bind返回值------------')
const bindReturn = fn.bind(obj, 1)
// 执行这个bind的返回值,就会在控制台进行输出
bindReturn()

实现call、apply和bind方法:

  • call方法的实现

    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
    function foo(...args){
    console.log('foo函数的this', this)
    console.log('foo函数的参数', args)
    }

    var obj = {
    a: 1,
    b: 2
    }
    // 使用call方法改变this
    foo.call(obj, 1, 2,3) // foo函数的this {a: 1, b: 2} // foo函数的参数 [1, 2, 3]
    // 想要实现call方法,从对象下手,想象使用call方法后调用的是如下的obj.foo()方法
    obj = {
    a: 1,
    b: 2,
    foo: function(...args){
    console.log('foo函数的this', this)
    console.log('foo函数的参数', args)
    }
    }

    // 于是可以实现call方法如下
    Function.prototype.myCall = function(){
    // 获取参数
    const context = [...arguments].slice(0,1)
    const rest = [...arguments].slice(1)
    if (!context) {
    //context为null或者是undefined
    context = typeof window === 'undefined' ? global : window
    }
    context.fn = this // 向参数对象中添加一个fn属性,表示当前调用call方法的方法,就像上面的obj对象添加的foo方法
    let result = context.fn(...rest) // 调用当前方法传入剩余参数
    delete context.fn
    return result // call方法是立即执行,所以返回执行结果
    }
  • apply方法的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // apply方法的实现与call方法类似,只是剩余参数的传递不是参数列表而是数组
    Function.prototype.myApply = function (context, args) { // apply中第二个参数为类数组
    if (!context) {
    //context为null或者是undefined
    context = typeof window === 'undefined' ? global : window
    }
    context.fn = this
    let result = args == undefined ? context.fn(args) : context.fn(...args) // 调用当前方法传入剩余参数
    delete context.fn
    return result // apply方法是立即执行,所以返回执行结果
    }
  • bind方法的实现

    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
    Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
    throw new TypeError('Error')
    }
    const _self = this
    const args = [...arguments].slice(1) // 剩余参数,一个数组
    const Fn = function () {} // 定义一个空函数
    Fn.prototype = this.prototype // 维护原型关系
    // bind返回一个函数
    const fnResult = function () {
    /*
    对三目运算符两种情况的解释:
    1.当作为构造函数时,this 指向实例(注意!!!这里的this是bind返回的新方法里执行时的this,
    和上面的this不是一个!!!),Fn 为绑定函数,因为上面的 `Fn.prototype = this.prototype;`,
    已经修改了 Fn.prototype 为 绑定函数的 prototype,此时结果为 true,
    当结果为 true 的时候,this 指向实例。

    2.当作为普通函数时,this 指向 window,Fn 为绑定函数,此时结果为 false,
    当结果为 false 的时候,this 指向绑定的 context。
    */
    const _this = this instanceof _self ? this : context
    const _args = [...args, ...arguments]
    // 且可以继续传参数,这里对参数进行拼接
    return _self.apply(_this, _args)
    }
    //原型链
    fnResult.prototype = new Fn()
    return fnResult
    }

ajax请求通过JS实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.创建请求实例对象
const xhr = new XMLHttpRequest()
// 2.建立服务器连接
xhr.open('GET', 'url', true)
// 3.发送数据
xhr.send() // get请求不发送,send是发送请求体的数据
// 4.监听请求的状态变化
xhr.onreadyStateChange = function() {
// 4表示请求完成
if(xhr.readyState === 4){
// 200状态码表示请求成功
if(xhr.status === 200){
...处理成功的操作
}else {
...处理失败的操作
}
}
}

使用XMLHttpRequest对象封装ajax方法

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
// 对象转url的参数字符串
function objToParams(obj) {
let paramsStr = ''
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const element = obj[key]
paramsStr.length > 0
? (paramsStr = paramsStr + '&' + `${key}=${element}`)
: (paramsStr = `${key}=${element}`)
}
}
return paramsStr
}

function ajax(request) {
// 初始化参数
request = request || {}
const method = (request.method || 'GET').toUpperCase()
const url = request.url
const params = JSON.stringify(request.data)

// 1.创建请求实例对象
const xhr = new XMLHttpRequest()
// 2.与服务器建立连接
// 3.向服务器发送数据
if (method === 'GET') {
xhr.open(method, url + '?' + objToParams(params))
xhr.send()
} else {
// POST、DELETE、PUT请求等
xhr.open(method, url)
xhr.send(params)
}
// 4.监听请求状态
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
request.success && request.success(xhr.response)
} else if (xhr.status >= 400) {
request.fail && request.fail(xhr.responseText)
}
}
}
}

// 调用ajax
ajax({
url: `http://jsonplaceholder.typicode.com/posts`,
method: 'get',
success: function (res) {
console.log(res)
},
fail: function (err) {
console.log(err)
}
})

深拷贝与浅拷贝实现

浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。

浅拷贝实现的是拷贝对象的内存地址,改变拷贝对象的属性时,原对象属性也会随之改变。

实现代码如下:

1
2
3
4
5
6
7
8
9
function shallowClone(obj){
const newObj = {}
for(var key in obj){
if(obj.hasOwnProperty(key)){
newObj[key] = obj[key]
}
}
return newObj
}

JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

  • 深拷贝实现1: JSON.stringify()。存在缺陷:会忽略undefined、symbol和函数类型的数据
1
2
3
4
function deepClone1(obj) {
let newObj = JSON.parse(JSON.stringify(obj))
return newObj
}
  • 深拷贝实现2: 循环递归
1
2
3
4
5
6
7
8
9
10
11
function deepClone2(obj) {
if (obj === null || typeof obj !== 'object') return obj
let newObj = new obj.constructor() // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身,新开辟空间
for (let key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
// 实现一个递归拷贝
newObj[key] = deepClone2(obj[key])
}
}
return newObj
}

instanceof关键字的实现

  1. instanceof的用法:

instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

1
2
3
4
5
6
7
8
// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false
  1. 实现instanceof的原理:
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
/**
* 实现instanceof关键字的原理
* @param {Any} left
* @param {constructor} right
* @return Boolean
*/
function instanceof2(left, right) {
// 如果left是基本数据类型或null,直接返回false
if (!['object', 'function'].includes(typeof left) || left === null) return false
// Object.getPrototypeOf方法返回指定对象的原型(内部[[Prototype]]属性的值)
let proto = Object.getPrototypeOf(left)
while (proto !== null) {
if (proto === right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}


// Object.getPrototypeOf拓展
Object.getPrototypeOf([1,2]) === Array.prototype // true
const prototype1 = {}
const object1 = Object.create(prototype1)
Object.getPrototypeOf(object1) === prototype1 // true
Object.getPrototypeOf( Object ) === Function.prototype // true

// Object.getPrototypeOf( Object )是把Object这一构造函数看作对象,
// 返回的当然是函数对象的原型,也就是 Function.prototype。

拓展:结合typeof和instanceof精确判断某一个变量的数据类型

  1. 实现通用的获取数据类型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}

getType([]) // "Array" typeof []是object,因此toString返回
getType('123') // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null) // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined) // "undefined" typeof 直接返回
getType() // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g) //"RegExp" toString返回

Promise实现原理

promise的使用

  1. promise对象一共只有三种状态:

    1、pending;2、fulfilled;3、rejected

    状态只允许从pending变化成fulfilled或从pending变化成rejected,且状态一旦改变,就无法回退。

  2. 通过promise的构造函数可以创建一个异步方法实例

    1
    const promise = new Promise(function(resolve, reject) {});
  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”
  1. promise对象常用的方法有thencatchfinally

    这里主要介绍then方法的使用,在实现Promise的原理时也着重实现then方法:

    then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

    then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

    1
    2
    3
    4
    5
    getJSON("/posts.json").then(function(json) {
    return json.post;
    }).then(function(post) {
    // ...
    });

开始实现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
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
class MyPromise{
state = 'pending' // 定义状态
result = undefined // 成功的结果
reason = undefined // 失败的结果
handleResolved = [] // 成功的回调函数
handleRejected = [] // 失败的回调函数
constructor(execute) {
const resolve = result => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.result = result
// 回调函数数组中如果收集到对应的回调函数,那么在执行对应的改变状态函数时,就会执行回调函数
this.handleResolved.forEach(fn => fn && fn(result))
}
}
const reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.handleRejected.forEach(fn => fn && fn(reason))
}
}
try {
execute(resolve, reject)
} catch (error) {
reject(error)
}
}

then(onResolved, onRejected) {
if (typeof onResolved !== 'function') {
onResolved = data => data
}
if (typeof onRejected !== 'function') {
onRejected = err => {
throw new Error(err)
}
}
const newPromise = new MyPromise((resolve, reject) => {
// 异步操作,收集resolve和reject函数
if (this.state === 'pending') {
this.handleResolved.push(() => {
const returnResult = onResolved(this.result)
// 链式调用的关键函数
handleReturnPromise(returnResult, newPromise, resolve, reject)
})
this.handleRejected.push(() => {
const returnResult = onRejected(this.reason)
// 链式调用的关键函数
handleReturnPromise(returnResult, newPromise, resolve, reject)
})
}
// 同步操作,直接执行对应的resolve和reject
if (this.state === 'fulfilled') {
setTimeout(() => {
const returnResult = onResolved(this.result)
// 链式调用的关键函数
handleReturnPromise(returnResult, newPromise, resolve, reject)
}, 0)
}
if (this.state === 'rejected') {
setTimeout(() => {
const returnResult = onRejected(this.reason)
// 链式调用的关键函数
handleReturnPromise(returnResult, newPromise, resolve, reject)
}, 0)
}
})
return newPromise
}

catch (onRejected) {
return this.then(undefined, onRejected);
}
}

// 链式调用的关键函数
function handleReturnPromise(result, newPromise, resolve, reject) {
if (result === newPromise) {
throw new Error('return oneself is not allowed')
}

if (typeof result === 'object' || typeof result === 'function') {
const then = result.then;
if (typeof then === 'function') {
then.call(result, (res) => {
handleReturnPromise(res, newPromise, resolve, reject)
}, rej => {
reject(rej)
})
} else {
resolve(result)
}
} else {
// return 的是基本类型数据直接resolve
resolve(result)
}
}

// 验证函数
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 1000)
})

p.then((res, err) => {
console.log(res)
console.log(err)
return '第一层promise的返回值'
}).then(res => {
console.log(res)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('第二层Promise的返回值,我在2秒后出现')
}, 1000)
})
}).then(res => {
console.log(res);
})

// 打印结果
// data
// undefined
// 第一层promise的返回值
// 二层Promise的返回值,我在2秒后出现