最近看到有些朋友去大厂面试时,面试官几乎都要求手撕原生代码 (真的手撕哦,不是仅仅懂原理)。这年头不会手撕原生代码,算法,都不好意思叫自己程序员 (=_=),揾食艰难。我自己有些慌了,所以把我所知的记录下来。
call
call是Function的一个原型方法,可以改变传入函数的this的指向。具体实现方法如下:
Function.prototype.mycall = function (context, ...args) {
context.fn = this
context.fn(...args)
context.fn = null
}
要点是为传入的上下文添加fn属性,以让它调用。在JS里,谁调用,this就指向谁。context.fn()
里的this是指向context,如果是 context.subcontext.fn()
,this是指向subcontext。
apply
apply也是类似的方法,只是apply传入的参数要求是数组。如果用...args可以把数组解构,这样的话实现方法与call是基本一样。
Function.prototype.mycall = function (context, args) {
context.fn = this
context.fn(...args)
context.fn = null
}
new
在实现bind之前,先研究new的实现,因为接下来的bind需要了解它的原理。当new 一个函数 (类)时,它做了什么?根据MDN,new的时候会发生以下事情:
通过上述内容,大概可以知道实现方法。
function mynew (fn, ...args) {
obj = {}
const result = fn.call(obj, ...args)
obj.__proto__ = fn.prototype
return result !== undefined ? result : obj
}
bind
bind的方法就有点不同。它是返回一个函数,这个函数的this指向传入的上下文,还有保存之前的参数。
先把上述要求实现吧。既然要保存传入的参数,自然要利用闭包。
Function.prototype.mybind = function (context, ...firstargs) {
const self = this
return function (...args) {
firstargs.concat(args)
self.call(context, ...firstargs.concat(args))
}
}
不过还要考虑bind的一个特性,就是
上述是MDN的官方描述。举个例:
const obj = {a: "12",b: "54", z: "hello world"}
function bind_demo (s) {
console.log (this.z)
this.s = s
}
const bind_fn = bind_demo.bind(obj, "added")
bind_fn() // hello world
console.log(obj) // { a: '12', b: '54', z: 'hello world', s: 'added' }
const new_obj = new bind_fn() // undefined
console.log(new_obj) // bind_demo { s: 'added' }
console.log(new_obj instanceof bind_fn) // true
如果直接调用bind后的函数,结果如我们所想的一样。然而当new了bind_fn后,情况发生变化,console.log (this.z)
的结果成了undefined,因为这时的this不是指向obj了,而是new所创建的对象,它是bind_demo的实例。所以应当添加一个判断,判断this是指向new创建的类,还是传入的上下文context。
Function.prototype.mybind = function (context, ...firstargs) {
const self = this
const fn_bind = function (...args) {
firstargs.concat(args)
self.call(this instanceof fn_bind ? this : context, ...firstargs.concat(args))
}
fn_bind.prototype = self.prototype
return fn_bind
}
别忘了,最后还要考虑继承函数的原型。
数组拍平
关于数组拍平,我第一时间想到的是用递归来做:
Array.prototype.myflatten = function () {
return recursiveflat (this, [])
}
function recursiveflat (arr, result) {
if (!Array.isArray(arr)) {
throw new Error ("The first parameter is not array.")
}
arr.forEach (item => {
if (Array.isArray(item)) {
recursiveflat (item, result)
} else {
result.push (item)
}
})
return result
}
不过能否不用递归呢? 这样效率会更高。可以利用some验测数组有没有数组元素,如果有,利用apply把传入的数组拍平一层,然后把数组与空数组合并。
function flat(arr){
while(arr.some(item => Array.isArray(item))){
arr = [].concat.apply([],arr); // apply会把传入的数组转换为参数
}
return arr;
}
var arr = [1,2,[3,4,5,[6,7,8],9],10,[11,12]];
flat(arr);
深拷贝
这个真的要用递归才能解决。深拷贝的代码实现不难,难是难在把所有情况考虑。一步一步实现吧。
先从浅拷贝着手:
function shallowClone (obj) {
const cloneObj = {}
const objKey = Object.keys(obj)
objKey.forEach (item => {
if (Object.prototype.hasOwnProperty.call(obj, item)) {
cloneObj[item] = obj[item]
}
})
return cloneObj
}
深拷贝只是在浅拷贝的基础上加上递归:
function deepClone (obj) {
const cloneObj = {}
const objKey = Object.keys(obj)
objKey.forEach (item => {
if (Object.prototype.hasOwnProperty.call(obj, item)) {
if (typeof obj[item] === 'object') {
cloneObj[item] = deepClone(obj[item])
} else {
cloneObj[item] = obj[item]
}
}
})
return cloneObj
}
不过还是有些问题:
- 由于
typeof null
是 object,所以还要考虑null的问题 - 没有考虑数组
我这里参考 木易杨 大神的写法,详细可以阅读他的实现:
【进阶4-3期】面试题之如何实现一个深拷贝
首先先解决null的问题,先封装一个真正的判断object的方法。
function isObject(obj) {
return typeof obj === 'object' && obj != null;
}
然后开始真正实现深拷贝:
function isObject(obj) {
return typeof obj === 'object' && obj != null;
}
function deepClone (obj) {
if(!isObject (obj)) {
return obj
}
const cloneObj = Array.isArray(obj) ? [] : {}
for (let item in obj) {
if (Object.prototype.hasOwnProperty.call(obj, item)) {
if (isObject (obj[item])) { // 数组也包含在内
cloneObj[item] = deepClone(obj[item])
} else {
cloneObj[item] = obj[item]
}
}
}
return cloneObj
}
柯里化
了解柯里化前,得要知道函数式编程。
所谓函数式编程是一种编程思想,简单来说,就是把程序里的函数封装成 纯函数,纯函数就是应用数学的函数概念:函数即映射。
因此往纯函数传入的参数,返回结果都是相同,好像平时做数学题,往函数代入某个数,出来的结果也是相同的。这样做有什麽好处?
最直观的结果就是可预测,不会如非函数般,每次调用的结果可能不同,例如栈stack的pop方法,从而使得程序更加健壮,更容易测试。还有就是写出来的程序更接近自然语言,例如:
operate (substract (3), add (4), multiply (10), 7)
即使不懂程序,很容易推断出这是做一系列运算的程序。
函数式编程的介绍就到这里,终于可以正式谈柯里化。 柯里化
引用维基百科的定义:
可以看一下lodash里柯里化函数的用法:
const _ = require('lodash')
// 要柯里化的函数
function getSum (a, b, c) { return a + b + c }
// 柯里化后的函数
let curried = _.curry(getSum)
// 测试
curried(1, 2, 3)
curried(1)(2)(3)
curried(1, 2)(3)
三者结局是一样的,第一个curried直接执行,其馀两个则是缓存参数后再执行。
我们可以利用闭包实现一个柯里化函数:
function currying (fn) {
return function curryfn (...args) {
if (fn.length > args.length) {
return function () {
return curryfn (...args.concat (Array.from (arguments)))
}
}
return fn (...args)
}
}
memorize
既然纯函数的返回结果是可预测,可以利用缓存,把之前传入参数的返回结果存下来,如果之后传入相同参数,则直接返回结果,不用调用函数。 Vue的computed就是运用这原理的。
function memorize (fn) {
const cache = {}
return function (arg) {
if (cache[arg]) {
return cache[arg]
}
cache[arg] = fn (arg)
return cache[arg]
}
}
函数组合
可以利用纯函数和柯里化的特点,把函数组合。通常组合的函数是从右至左执行。组合的实现如下:
function flowRight (...args) {
return function (initValue) {
return args.reverse().reduce ((acc, fn) => fn (acc), initValue)
}
}
const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const f = flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three'])) // THREE
防抖与节流
这两个概念很容易混淆,因为它们要实现的功能都很相似,都是为了防止用户在一段时间内频繁调用函数,不同之处是防抖是指定某个时间点调用函数,如果在该时间点前再次调用,则取消之前的调用函数。
节流则是调用函数后,在一定时间内不许再次调用。
这里参考朱德龙老师在前端高手进阶的写法,他的写法是我见过最全面的,详情可阅读 3 个使用场景助你用好 DOM 事件
先把防抖实现吧。通常网上的写法是:
function deBounce (fn, wait = 0) {
let timeout = null
return function deBounced (...args) {
if (timeout) {
clearTimeout (timeout)
timeout = null
}
setTimeout (fn(...args), wait)
}
}
这样写也没错,不过没有考虑
- 函数之后是否需要回调其他函数
- 是否需要手动直接调用
- 取消调用函数
朱德龙老师的代现实现是:
const debounce = (func, wait = 0) => {
let timeout = null
let args
function debounced(...arg) {
args = arg
if(timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函数执行结果
return new Promise((res, rej) => {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch(e) {
rej(e)
}
}, wait)
})
}
// 允许取消
function cancel() {
clearTimeout(timeout)
timeout = null
}
// 允许立即执行
function flush() {
cancel()
return func.apply(this, args)
}
debounced.cancel = cancel
debounced.flush = flush
return debounced
}
面试应该不会考得这么细,不过值得学习一下。
函数节流主要有两种实现方法:时间戳和定时器。时间戳就是规定时间内只能调用一次,在该时间内再次调用无效。
时间戳:
function throttle1 (fn, wait = 0) {
let lastTime = new Date().getTime()
return function (...args) {
if (lastTIme - new Date().getTime() >= wait) {
lastTime = new Date().getTime()
fn.apply (fn, args)
}
}
}
定时器指的是规定时间只能调用一次函数,如果规定时间内再次调用,则把它放在下一个规定时间调用。代码实现如下:
const throttle = (func, wait = 0) => {
let timeout = null
let args
let firstCallTimestamp
function throttled(...arg) {
if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
if (!args) {
console.log('set args:', arg)
args = arg
}
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函数执行结果
return new Promise(async(res, rej) => {
if (new Date().getTime() - firstCallTimestamp >= wait) {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
} else {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
}, firstCallTimestamp + wait - new Date().getTime()) // 计算下一个指定时间
}
})
}
// 允许取消
function cancel() {
clearTimeout(timeout)
args = null
timeout = null
firstCallTimestamp = null
}
// 允许立即执行
function flush() {
cancel()
return func.apply(this, args)
}
throttled.cancel = cancel
throttled.flush = flush
return throttled
}
Promise
这个我之前写过,放过链接:
Promise的实现原理
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!