前言
大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
对于手写题的理解:
手写题其实对面试者的要求会高一点,因为不仅要考我们对于 API 的使用情况,还考我们对原理的理解,而且还要求写出来。所以我这里的建议是:
- 首先对于考点要知道是干嘛的,比如让实现一个柯里化函数,如果不知道什么是柯里化那肯定是实现不了的。
- 其次要对其的使用了如指掌,比如考
forEach
的实现,很多人都不知道forEach
第一个参数callbackFn
其实是有三个参数的,另外forEach
还有一个可选的thisArg
参数。 - 最后才是对原理的理解,建议可以记思路但不要死记代码哈。
- 写完之后可以多检测,想想是否还有一些边界情况值得考虑,会给面试官好的映象。
1. 实现一个 compose 函数
compose
是组合的意思,它的作用故名思议就是将多个函数组合起来调用。
我们可以把 compose
理解为了方便我们连续执行方法,把自己调用传值的过程封装了起来,我们只需要给 compose
函数我们要执行哪些方法,他会自动的执行。
实现:
const add1 = (num) => { return num + 1; }; const mul5 = (num) => { return num * 5; }; const sub8 = (num) => { return num - 8; }; const compose = function (...args) { return function (num) { return args.reduceRight((res, cb) => cb(res), num); }; }; console.log(compose(sub8, mul5, add1)(1)); // 2 复制代码
上面的例子,我要将一个数加1乘5再减8,可以一个一个函数调用,但是那样会很麻烦,使用 compose
会简单很多。
compose
在函数式编程非常常见,Vue
中就有很多地方使用 compose
,学会它有助于我们阅读源码。
注意 compose
是从右往左的顺序执行函数的,下面说的 pipe
函数 是从左往右的顺序执行函数。 pipe
和 compose
都是函数式编程中的基本概念,能让代码变得更好~~
2. 实现一个 pipe 函数
pipe
函数和 compose
函数功能一样,只不过是从左往右执行顺序,只用将上面 reduceRight
方法改为 reduce
即可
const add1 = (num) => { return num + 1; }; const mul5 = (num) => { return num * 5; }; const sub8 = (num) => { return num - 8; }; const compose = function (...args) { return function (num) { return args.reduce((res, cb) => cb(res), num); }; }; console.log(compose(sub8, mul5, add1)(1)); // -34 复制代码
3. 实现一个 forEach 函数
forEach()
方法能对数组的每个元素执行一次给定的函数。
需要注意的点有,
- 挂载到
Array
的原型上 - 具有
callBackFn
,thisArg
两个参数 callBackFn
是一个函数, 且具有三个参数- 返回
undefined
Array.prototype.myForEach = function (callBackFn, thisArg) { if (typeof callBackFn !== "function") { throw new Error("callBackFn must be function"); } thisArg = thisArg || this; const len = this.length; for (let i = 0; i < len; i++) { callBackFn.call(thisArg, this[i], i, this); } }; 复制代码
4. 实现一个 map 函数
map()
方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。
需要注意的点有,
- 挂载到
Array
的原型上 - 具有
callBackFn
,thisArg
两个参数 callBackFn
是一个函数, 且具有三个参数- 返回一个新数组,每个元素都是回调函数的返回值
Array.prototype.myMap = function (callbackFn, thisArg) { if (typeof callbackFn !== "function") { throw new Error("callbackFn must be function"); } const arr = []; thisArg = thisArg || this; const len = this.length; for (let i = 0; i < len; i++) { arr.push(callbackFn.call(thisArg, this[i], i, this)); } return arr; }; 复制代码
5. 实现一个 filter 函数
filter()
方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。
需要注意的点有,
- 挂载到
Array
的原型上 - 具有
callBackFn
,thisArg
两个参数 callBackFn
是一个函数, 且具有三个参数- 返回一个新数组,每个元素都需要通过回调函数测试,且为浅拷贝
Array.prototype.myFilter = function (callbackFn, thisArg) { if (typeof callbackFn !== "function") { throw new Error("must be function"); } const len = this.length; thisArg = thisArg || this const _newArr = []; for (let i = 0; i < len; i++) { if (callbackFn.call(thisArg, this[i], i, this)) { if (typeof this[i] === "object") { _newArr.push(Object.create(this[i])); } else { _newArr.push(this[i]); } } } return _newArr; }; 复制代码
6. 自定义函数:在对象中找出符合规则的属性
这个其实跟 filter
挺像的,只不过一个是在数组中过滤元素,一个是在对象中过滤属性。
需要注意的点有,
- 挂载到
Object
的原型上 - 具有
callBackFn
,thisArg
两个参数 callBackFn
是一个函数, 且具有三个参数- 返回一个新数组,每个元素都需要通过回调函数测试,且为对象的属性名。
Object.prototype.filterProperty = function (callbackFn, thisArg) { if (typeof callbackFn !== "function") { throw new Error("must be function"); } thisArg = thisArg || this; const propArray = []; for (let prop in this) { if (callbackFn.call(thisArg, prop, this[prop], this)) { propArray.push(prop); } } return propArray; }; 复制代码
7. 实现一个 bind 方法
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
需要注意的点有,
- 挂载到
Function
的原型上 - 具有 thisArgs 和 原函数所需的其他参数
thisArgs
传递的任何值都需转换为对象bind
中输入的原函数所需的参数需要在返回函数中能接上,意思就是下面两种方式都要支持
foo.bind(obj,1,2)(3) foo.bind(obj,1,2,3) 复制代码
- 返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
Function.prototype.myBind = function (thisArgs, ...args1) { thisArgs = Object(thisArgs) const _self = this; // const args1 = Array.prototype.slice.call(arguments, 1); return function (...args2) { // const args2 = Array.prototype.slice.call(arguments, 1); return _self.apply(thisArgs, args1.concat(args2)); }; }; 复制代码
8. 实现一个 call 方法
call()
方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
需要注意的点有,
- 挂载到
Function
的原型上 - 具有
thisArgs
和 原函数所需的其他参数 - 需要判断
thisArgs
是否为undefined
, 如果为undefined
要他赋值全局的对象。 - 返回 函数调用结果
Function.prototype.myCall = function (thisArg, ...args) { if (thisArg) { thisArg = Object(thisArg); } else { thisArg = typeof window !== "undefined" ? window : global; } thisArg._fn = this; const result = thisArg._fn(...args); delete thisArg._fn; return result; }; 复制代码
9. 实现一个 apply 方法
apply()
方法调用一个具有给定 this
值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。
Function.prototype.myApply = function (thisArg, args) { if (thisArg) { thisArg = Object(thisArg); } else { thisArg = typeof window !== "undefined" ? window : global; } let result; if (!args) { result = thisArg._fn(); } else { result = thisArg._fn(...args); } delete thisArg._fn; return result; }; 复制代码
:::note{title="备注"} 虽然这个函数的语法与 call()
几乎相同,但根本区别在于,call()
接受一个参数列表,而 apply()
接受一个参数的单数组。 :::
10. 实现一个能自动柯里化的函数
10.1 什么是柯里化?
- 柯里化(英语:Currying)是函数式编程里一个非常重要的概念。
- 是把接受多个参数的函数,变成接受一个单一参数的函数,并且会返回一个函数,这个返回函数接收余下的参数。
- 柯里化声称 "如果你固定某些参数,你将得到接受余下参数的一个函数"
举个例子
下面有两个函数 foo
和 bar
,他们两个调用的结果都相同, 1 + 2 + 3 + 4 = 10,但是调用的方式却不同。
function foo(a,b,c,d) { return a + b + c + d } function bar(a) { return function(b) { return function(c) { return function(d) { return a + b + c + d } } } foo(1,2,3,4) // 10 bar(1)(2)(3)(4) // 10 复制代码
将函数foo变成bar函数的过程就叫柯里化
上面的 bar
函数还可以简写成
const bar = a => b => c => d => a + b + c + d 复制代码
10.2 为什么需要柯里化?
10.2.1 单一职责的原则
- 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;
- 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后就在下一个函数中再使用处理后的结果
我们把上面的例子改一下 现在我们需要对函数参数做一些操作,a = a +2
, b = b * 2
, c = -c
如果全都写在一个函数中是下面这样的
function add (a,b,c) { a = a + 2 b = b * 2 c = -c return a + b + c } 复制代码
而柯里化后
function add(a,b,c) { a = a + 2 return function(b,c) { b = b * 2 return function(c) { c = -c return a + b + c } } } 复制代码
很明显,柯里化后的函数单一性更强了,比如在最外层函数的逻辑就是对a进行操作,第二层函数就是对b进行操作,最内层就是对c进行操作
这是柯里化的第一个好处:更具有单一性
10.2.2 逻辑的复用
我们简化一下第一个例子,现在需要一个加法函数
function add(a,b){ return a + b } add(5,1) add(5,2) add(5,3) add(5,4) 复制代码
可以发现每次都是5加上另一个数字,每次都要传5其实是没必要的
- 柯里化的优化
function add(a,b) { // 复用的逻辑 console.log("+",a) return function(b) { return a + b } } const add5 = add(5) add5(2) add5(3) add5(5) 复制代码
可以看到在外层的函数中的代码被复用了,也可以说是定制化了一个加5的函数
10.3. 最终实现
- 上面的几个例子都是我们手动写的柯里化函数。
- 有没有办法写一个函数 传入一个函数自动的返回一个柯里化函数?
(终于到了手写了^_^)
- 我们要实现一个函数,这个函数能将普通的函数转换成柯里化函数,所以这个函数的框架就有了。
function currying(fn) { function curried(...args) { } return curried } 复制代码
- 因为原函数的参数我们不确定,所以需要递归的组合原函数的参数,直到 curried函数的参数 等于原函数fn的参数长度时,结束递归。
function currying(fn) { function curried(...args) { // 判断当前已接收的参数个数是否与fn函数一致 // 1.当已经传入的参数数量 大于等于 需要的参数时,就执行函数 if(args.length >= fn.length) { return fn.apply(this, args) } else { // 2.没达到个数时,需要返回一个新的函数,继续来接受参数 return function curried2(...args2) { // 接收到参数后,需要递归调用curried来检查函数的个数是否达到 return curried.apply(this, [...args, ...args2]) } } } return curried } 复制代码
- 测试
function add (a,b,c,d) { return a + b + c + d } const curryingFn = currying(add); console.log(add(1, 2, 3, 4)); // 10 console.log(curryingFn(1)(2)(3)(4)); // 10 console.log(curryingFn(1, 2)(3)(4)); // 10 console.log(curryingFn(1, 2, 3)(4)); // 10 console.log(curryingFn(1, 2, 3, 4)); // 10 复制代码