《前端面试手记》之常考的源码实现

31次阅读

共计 5819 个字符,预计需要花费 15 分钟才能阅读完成。

???? 内容速览 ????

手动实现 call/apply/bind

实现一个完美的深拷贝函数
基于 ES5/ES6 实现双向绑定

instanceof 原理与实现

???? 查看全部教程 / 阅读原文????
手动撸个 call/apply/bind
实现 call
来看下 call 的原生表现形式:
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}

run.call({
a: ‘a’,
b: ‘b’
}, 1, 2)
好了,开始手动实现我们的 call2。在实现的过程有个关键:
如果一个函数作为一个对象的属性,那么通过对象的. 运算符调用此函数,this 就是此对象
let obj = {
a: ‘a’,
b: ‘b’,
test: function (arg1, arg2) {
console.log(arg1, arg2)
// this.a 就是 a; this.b 就是 b
console.log(this.a, this.b)
}
}

obj.test(1, 2)
知道了实现关键,下面就是我们模拟的 call:
Function.prototype.call2 = function(context) {
if(typeof this !== ‘function’) {
throw new TypeError(‘Error’)
}

// 默认上下文是 window
context = context || window
// 保存默认的 fn
const {fn} = context

// 前面讲的关键,将函数本身作为对象 context 的属性调用,自动绑定 this
context.fn = this
const args = […arguments].slice(1)
const result = context.fn(…args)

// 恢复默认的 fn
context.fn = fn
return result
}

// 以下是测试代码
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}

test.call2({
a: ‘a’,
b: ‘b’
}, 1, 2)
实现 apply
apply 和 call 实现类似,只是传入的参数形式是数组形式,而不是逗号分隔的参数序列。
因此,借助 es6 提供的 … 运算符,就可以很方便的实现数组和参数序列的转化。
Function.prototype.apply2 = function(context) {
if(typeof this !== ‘function’) {
throw new TypeError(‘Error’)
}

context = context || window
const {fn} = context

context.fn = this
let result
if(Array.isArray(arguments[1])) {
// 通过 … 运算符将数组转换为用逗号分隔的参数序列
result = context.fn(…arguments[1])
} else {
result = context.fn()
}

context.fn = fn
return result
}

/**
* 以下是测试代码
*/

function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}

test.apply2({
a: ‘a’,
b: ‘b’
}, [1, 2])
实现 bind
bind 的实现有点意思,它有两个特点:

本身返回一个新的函数,所以要考虑 new 的情况
可以“保留”参数,内部实现了参数的拼接

Function.prototype.bind2 = function(context) {
if(typeof this !== ‘function’) {
throw new TypeError(‘Error’)
}

const that = this
// 保留之前的参数,为了下面的参数拼接
const args = […arguments].slice(1)

return function F() {
// 如果被 new 创建实例,不会被改变上下文!
if(this instanceof F) {
return new that(…args, …arguments)
}

// args.concat(…arguments): 拼接之前和现在的参数
// 注意:arguments 是个类 Array 的 Object, 用解构运算符 …, 直接拿值拼接
return that.apply(context, args.concat(…arguments))
}
}

/**
* 以下是测试代码
*/

function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}

const test2 = test.bind2({
a: ‘a’,
b: ‘b’
}, 1) // 参数 1

test2(2) // 参数 2
实现一个完美的深拷贝函数
实现一个对象的深拷贝函数,需要考虑对象的元素类型以及对应的解决方案:

基础类型:这种最简单,直接赋值即可
对象类型:递归调用拷贝函数
数组类型:这种最难,因为数组中的元素可能是基础类型、对象还可能数组,因此要专门做一个函数来处理数组的深拷贝

/**
* 数组的深拷贝函数
* @param {Array} src
* @param {Array} target
*/
function cloneArr(src, target) {
for(let item of src) {
if(Array.isArray(item)) {
target.push(cloneArr(item, []))
} else if (typeof item === ‘object’) {
target.push(deepClone(item, {}))
} else {
target.push(item)
}
}
return target
}

/**
* 对象的深拷贝实现
* @param {Object} src
* @param {Object} target
* @return {Object}
*/
function deepClone(src, target) {
const keys = Reflect.ownKeys(src)
let value = null

for(let key of keys) {
value = src[key]

if(Array.isArray(value)) {
target[key] = cloneArr(value, [])
} else if (typeof value === ‘object’) {
// 如果是对象而且不是数组, 那么递归调用深拷贝
target[key] = deepClone(value, {})
} else {
target[key] = value
}
}

return target
}
这段代码是不是比网上看到的多了很多?因为考虑很周全,请看下面的测试用例:
// 这个对象 a 是一个囊括以上所有情况的对象
let a = {
age: 1,
jobs: {
first: “FE”
},
schools: [
{
name: ‘shenda’
},
{
name: ‘shiyan’
}
],
arr: [
[
{
value: ‘1’
}
],
[
{
value: ‘2’
}
],
]
};

let b = {}
deepClone(a, b)

a.jobs.first = ‘native’
a.schools[0].name = ‘SZU’
a.arr[0][0].value = ‘100’

console.log(a.jobs.first, b.jobs.first) // output: native FE
console.log(a.schools[0], b.schools[0]) // output: {name: ‘SZU’} {name: ‘shenda’}
console.log(a.arr[0][0].value, b.arr[0][0].value) // output: 100 1
console.log(Array.isArray(a.arr[0])) // output: true
看到测试用例,应该会有人奇怪为什么最后要输出 Array.isArray(a.arr[0])。这主要是因为网上很多实现方法没有针对 array 做处理,直接将其当成 object,这样拷贝后虽然值没问题,但是 array 的元素会被转化为 object。这显然是错误的做法。
而上面所说的深拷贝函数就解决了这个问题。
基于 ES5/ES6 实现“双向绑定”
要想实现,就要先看看什么是“双向数据绑定”,它和“单向数据绑定”有什么区别?这样才能知道要实现什么效果嘛。
双向绑定:视图(View)的变化能实时让数据模型(Model)发生变化,而数据的变化也能实时更新到视图层。
单向数据绑定:只有从数据到视图这一方向的关系。
ES5 的 Object.defineProperty
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title>Document</title>
<script>
const obj = {
value: ”
}

function onKeyUp(event) {
obj.value = event.target.value
}

// 对 obj.value 进行拦截
Object.defineProperty(obj, ‘value’, {
get: function() {
return value
},
set: function(newValue) {
value = newValue
document.querySelector(‘#value’).innerHTML = newValue // 更新视图层
document.querySelector(‘input’).value = newValue // 数据模型改变
}
})
</script>
</head>
<body>
<p>
值是:<span id=”value”></span>
</p>
<input type=”text” onkeyup=”onKeyUp(event)”>
</body>
</html>
ES6 的 Proxy
随着,vue3.0 放弃支持了 IE 浏览器。而且 Proxy 兼容性越来越好,能支持 13 种劫持操作。
因此,vue3.0 选择使用 Proxy 来实现双向数据绑定,而不再使用 Object.defineProperty。
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title>Document</title>
<script>
const obj = {}

const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, value, receiver) {
if(key === ‘value’) {
document.querySelector(‘#value’).innerHTML = value
document.querySelector(‘input’).value = value
}
return Reflect.set(target, key, value, receiver)
}
})

function onKeyUp(event) {
newObj.value = event.target.value
}

</script>
</head>
<body>
<p>
值是:<span id=”value”></span>
</p>
<input type=”text” onkeyup=”onKeyUp(event)”>
</body>
</html>
instanceof 原理与实现
instanceof 是通过原型链来进行判断的,所以只要不断地通过访问__proto__,就可以拿到构造函数的原型 prototype。直到 null 停止。
/**
* 判断 left 是不是 right 类型的对象
* @param {*} left
* @param {*} right
* @return {Boolean}
*/
function instanceof2(left, right) {
let prototype = right.prototype;

// 沿着 left 的原型链, 看看是否有何 prototype 相等的节点
left = left.__proto__;
while(1) {
if(left === null || left === undefined) {
return false;
}
if(left === prototype) {
return true;
}
left = left.__proto__;
}
}

/**
* 测试代码
*/

console.log(instanceof2([], Array)) // output: true

function Test(){}
let test = new Test()
console.log(instanceof2(test, Test)) // output: true
更多系列文章
⭐在 GitHub 上收藏 / 订阅⭐
《前端知识体系》

谈谈 promise/async/await 的执行顺序与 V8 引擎的 BUG
前端面试中常考的源码实现
Flex 上手与实战
……

《设计模式手册》

单例模式
策略模式
代理模式
迭代器模式
订阅 - 发布模式
桥接模式
备忘录模式
模板模式
……

《Webpack4 渐进式教程》

webpack4 系列教程 (一): 打包 JS
webpack4 系列教程 (二): 编译 ES6
webpack4 系列教程 (三): 多页面解决方案 – 提取公共代码
webpack4 系列教程 (四): 单页面解决方案 – 代码分割和懒加载
webpack4 系列教程 (五): 处理 CSS
webpack4 系列教程 (十五):开发模式与 webpack-dev-server
……

⭐在 GitHub 上收藏 / 订阅⭐

正文完
 0