2024年前端最新彻底搞懂闭包,柯里化,手写代码,金九银十不再丢分!,2024年最新面试几个月还没找到工作怎么办

最后

如果你已经下定决心要转行做编程行业,在最开始的时候就要对自己的学习有一个基本的规划,还要对这个行业的技术需求有一个基本的了解。有一个已就业为目的的学习目标,然后为之努力,坚持到底。如果你有幸看到这篇文章,希望对你有所帮助,祝你转行成功。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

剩余参数通过剩余语法...将多个参数聚合成一个数组。

function add(a, …args) {

return args.reduce((prev, curr) => {

return prev + curr

}, a)

}

剩余参数和arguments对象之间的区别主要有三个:

  • 剩余参数只包含那些没有对应形参的实参,而arguments对象包含了传给函数的所有实参。

  • arguments对象不是一个真正的数组,而剩余参数是真正的Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如sortmapforEachpop。而arguments需要借用call来实现,比如[].slice.call(arguments)

  • arguments对象还有一些附加的属性(如callee属性)。

剩余语法和展开运算符看起来很相似,然而从功能上来说,是完全相反的。

剩余语法(Rest syntax) 看起来和展开语法完全相同,不同点在于, 剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。

arguments

函数的实际参数会被保存在一个类数组对象arguments中。

类数组(ArrayLike)对象具备一个非负的length属性,并且可以通过从0开始的索引去访问元素,让人看起来觉得就像是数组,比如NodeList,但是类数组默认没有数组的那些内置方法,比如push, pop, forEach, map

我们可以试试,随便找一个网站,在控制台输入:

var linkList = document.querySelectorAll(‘a’)

会得到一个NodeList,我们也可以通过数字下标去访问其中的元素,比如linkList[0]

但是NodeList不是数组,它是类数组。

Array.isArray(linkList); // false

回到主题,arguments也是类数组,argumentslength实参的数量决定,而不是由形参的数量决定。

function add(a, b) {

console.log(arguments.length);

return a + b;

}

add(1, 2, 3, 4);

// 这里打印的是4,而不是2

arguments也是一个和严格模式有关联的对象。

  • 非严格模式下,arguments里的元素和函数参数都是指向同一个值的引用,对arguments的修改,会直接影响函数参数。

function test(obj) {

arguments[0] = ‘传入的实参是一个对象,但是被我变成字符串了’

console.log(obj)

}

test({name: ‘jack’})

// 这里打印的是字符串,而不是对象

  • 严格模式下,arguments是函数参数的副本,对arguments的修改不会影响函数参数。但是arguments不能重新被赋值,关于这一点,我在解读闭包,这次从ECMAScript词法环境,执行上下文说起这篇文章中解读不可变绑定时有提到。在严格模式下,也不能使用arguments.callerarguments.callee,限制了对调用栈的检测能力。

函数体


函数体(FunctionBody)是函数的主体,其中的函数代码(function code)由一对花括号{}包裹。函数体可以为空,也可以由任意条javascript语句组成。

函数的调用形式


大体来说,函数的调用形式分为以下四种:

作为普通函数

函数作为普通函数被调用,这是函数调用的常用形式。

function add(a, b) {

return a + b;

}

add(); // 调用add函数

作为普通函数调用时,如果在非严格模式下,函数执行时,this指向全局对象,对于浏览器而言则是window对象;如果在严格模式下,this的值则是undefined

作为对象的方法

函数也可以作为对象的成员,这种情况下,该函数通常被称为对象方法。当函数作为对象的方法被调用时,this指向该对象,此时便可以通过this访问对象的其他成员变量或方法。

var counter = {

num: 0,

increase: function() {

this.num++;

}

}

counter.increase();

作为构造函数

函数配合new关键字使用时就成了构造函数。构造函数用于实例化对象,构造函数的执行过程大致如下:

  1. 首先创建一个新对象,这个新对象的__proto__属性指向构造函数的prototype属性。

  2. 此时构造函数的this指向这个新对象。

  3. 执行构造函数中的代码,一般是通过this给新对象添加新的成员属性或方法。

  4. 最后返回这个新对象。

实例化对象也可以通过一些技巧来简化,比如在构造函数中显示地return另一个对象,jQuery很巧妙地利用了这一点。具体分析详见面试官真的会问:new的实现以及无new实例化。

通过call, apply调用

applycall是函数对象的原型方法,挂载于Function.prototype。利用这两个方法,我们可以显示地绑定一个this作为调用上下文,同时也可以设置函数调用时的参数。

applycall的区别在于:提供参数的形式不同,apply方法接受的是一个参数数组call方法接受的是参数列表

someFunc.call(obj, 1, 2, 3)

someFunc.apply(obj, [1, 2, 3])

注意,在非严格模式下使用call或者apply时,如果第一个参数被指定为nullundefined,那么函数执行时的this指向全局对象(浏览器环境中是window);如果第一个参数被指定为原始值,该原始值会被包装。这部分内容在下文中的手写代码会再次讲到。

call是用来实现继承的重要方法。在子类构造函数中,通过call来调用父类构造函数,以使对象实例获得来自父类构造函数的属性或方法。

function Father() {

this.nationality = ‘Han’;

};

Father.prototype.propA = ‘我是父类原型上的属性’;

function Child() {

Father.call(this);

};

Child.prototype.propB = ‘我是子类原型上的属性’;

var child = new Child();

child.nationality; // “Han”

call, apply, bind

=================

callapplybind都可以绑定this,区别在于:applycall是绑定this后直接调用该函数,而bind会返回一个新的函数,并不直接调用,可以由程序员决定调用的时机。

bind的语法形式如下:

function.bind(thisArg[, arg1[, arg2[, …]]])

bindarg1, arg2, ...是给新函数预置好的参数(预置参数是可选的)。当然新函数在执行时也可以继续追加参数。

手写call, apply, bind

===================

提到callapplybind总是无法避免手写代码这个话题。手写代码不仅仅是为了应付面试,也是帮助我们理清思路和深入原理的一个好方法。手写代码一定不要抄袭,如果实在没思路,可以参考下别人的代码整理出思路,再自己按照思路独立写一遍代码,然后验证看看有没有缺陷,这样才能有所收获,否则忘得很快,只能短时间应付应付。

那么如何才能顺利地手写代码呢?首先是要清楚一段代码的作用,可以从官方对于它的定义和描述入手,同时还要注意一些特殊情况下的处理。

就拿call来说,call是函数对象的原型方法,它的作用是绑定this和参数,并执行函数。调用形式如下:

function.call(thisArg, arg1, arg2, …)

那么我们慢慢来实现它,将我们要实现的函数命名为myCall。首先myCall是一个函数,接受的第一个参数thisArg是目标函数执行时的this的值,从第二个可选参数arg1开始的其他参数将作为目标函数执行时的实参。

这里面有很多细节要考虑,我大致罗列了一下:

  1. 要考虑是不是严格模式。如果是非严格模式,对于thisArg要特殊处理。

  2. 如何判断严格模式?

  3. thisArg被处理后还要进行非空判断,然后考虑是以方法的形式调用还是以普通函数的形式调用。

  4. 目标函数作为方法调用时,如何不覆盖对象的原有属性?

实现代码如下,请仔细看我写的注释,这是主要的思路!

// 首先apply是Function.prototype上的一个方法

Function.prototype.myCall = function() {

// 由于目标函数的实参数量是不定的,这里就不写形参了

// 实际上通过arugments对象,我们能拿到所有实参

// 第一个参数是绑定的this

var thisArg = arguments[0];

// 接着要判断是不是严格模式

var isStrict = (function(){return this === undefined}())

if (!isStrict) {

// 如果在非严格模式下,thisArg的值是null或undefined,需要将thisArg置为全局对象

if (thisArg === null || thisArg === undefined) {

// 获取全局对象时兼顾浏览器环境和Node环境

thisArg = (function(){return this}())

} else {

// 如果是其他原始值,需要通过构造函数包装成对象

var thisArgType = typeof thisArg

if (thisArgType === ‘number’) {

thisArg = new Number(thisArg)

} else if (thisArgType === ‘string’) {

thisArg = new String(thisArg)

} else if (thisArgType === ‘boolean’) {

thisArg = new Boolean(thisArg)

}

}

}

// 截取从索引1开始的剩余参数

var invokeParams = […arguments].slice(1);

// 接下来要调用目标函数,那么如何获取到目标函数呢?

// 实际上this就是目标函数,因为myCall是作为一个方法被调用的,this当然指向调用对象,而这个对象就是目标函数

// 这里做这么一个赋值过程,是为了让语义更清晰一点

var invokeFunc = this;

// 此时如果thisArg对象仍然是null或undefined,那么说明是在严格模式下,并且没有指定第一个参数或者第一个参数的值本身就是null或undefined,此时将目标函数当成普通函数执行并返回其结果即可

if (thisArg === null || thisArg === undefined) {

return invokeFunc(…invokeParams)

}

// 否则,让目标函数成为thisArg对象的成员方法,然后调用它

// 直观上来看,可以直接把目标函数赋值给对象属性,比如func属性,但是可能func属性本身就存在于thisArg对象上

// 所以,为了防止覆盖掉thisArg对象的原有属性,必须创建一个唯一的属性名,可以用Symbol实现,如果环境不支持Symbol,可以通过uuid算法来构造一个唯一值。

var uniquePropName = Symbol(thisArg)

thisArg[uniquePropName] = invokeFunc

// 返回目标函数执行的结果

return thisArguniquePropName

}

写完又思考了一阵,我突然发现有个地方考虑得有点多余了。

// 如果在非严格模式下,thisArg的值是null或undefined,需要将thisArg置为全局对象

if (thisArg === null || thisArg === undefined) {

// 获取全局对象时兼顾浏览器环境和Node环境

thisArg = (function(){return this}())

} else {

其实这种情况下不用处理thisArg,因为代码执行到该函数后面部分,目标函数会被作为普通函数执行,那么this自然指向全局对象!所以这段代码可以删除了!

接着来测试一下myCall是否可靠,我写了一个简单的例子:

function test(a, b) {

var args = [].slice.myCall(arguments)

console.log(arguments, args)

}

test(1, 2)

var obj = {

name: ‘jack’

};

var name = ‘global’;

function getName() {

return this.name;

}

getName();

getName.myCall(obj);

我不敢保证我写的这个myCall方法没有bug,但也算是考虑了很多情况了。就算是在面试过程中,面试官主要关注的就是你的思路和考虑问题的全面性,如果写到这个程度还不能让面试官满意,那也无能为力了…

理解了手写call之后,手写apply也自然触类旁通,只要注意两点即可。

  • myApply接受的第二个参数是数组形式。

  • 要考虑实际调用时不传第二个参数或者第二个参数不是数组的情况。

直接上代码:

Function.prototype.myApply = function(thisArg, params) {

var isStrict = (function(){return this === undefined}())

if (!isStrict) {

var thisArgType = typeof thisArg

if (thisArgType === ‘number’) {

thisArg = new Number(thisArg)

} else if (thisArgType === ‘string’) {

thisArg = new String(thisArg)

} else if (thisArgType === ‘boolean’) {

thisArg = new Boolean(thisArg)

}

}

var invokeFunc = this;

// 处理第二个参数

var invokeParams = Array.isArray(params) ? params : [];

if (thisArg === null || thisArg === undefined) {

return invokeFunc(…invokeParams)

}

var uniquePropName = Symbol(thisArg)

thisArg[uniquePropName] = invokeFunc

return thisArguniquePropName

}

用比较常用的Math.max来测试一下:

Math.max.myApply(null, [1, 2, 4, 8]);

// 结果是8

接下来就是手写bind了,首先要明确,bindcall, apply的不同点在哪里。

  • bind返回一个新的函数。

  • 这个新的函数可以预置参数。

好的,按照思路开始写代码。

Function.prototype.myBind = function() {

// 保存要绑定的this

var boundThis = arguments[0];

// 获得预置参数

var boundParams = [].slice.call(arguments, 1);

// 获得绑定的目标函数

var boundTargetFunc = this;

if (typeof boundTargetFunc !== ‘function’) {

throw new Error(‘绑定的目标必须是函数’)

}

// 返回一个新的函数

return function() {

// 获取执行时传入的参数

var restParams = [].slice.call(arguments);

// 合并参数

var allParams = boundParams.concat(restParams)

// 新函数被执行时,通过执行绑定的目标函数获得结果,并返回结果

return boundTargetFunc.apply(boundThis, allParams)

}

}

本来写到这觉得已经结束了,但是翻到一些资料,都提到了手写bind需要支持new调用。仔细一想也对,bind返回一个新的函数,这个函数被作为构造函数使用也是很有可能的。

我首先思考的是,能不能直接判断一个函数是不是以构造函数的形式执行的呢?如果能判断出来,那么问题就相对简单了。

于是我想到构造函数中很重要的一点,那就是在构造函数中,this指向对象实例。所以,我利用instanceof改了一版代码出来。

Function.prototype.myBind = function() {

var boundThis = arguments[0];

var boundParams = [].slice.call(arguments, 1);

var boundTargetFunc = this;

if (typeof boundTargetFunc !== ‘function’) {

throw new Error(‘绑定的目标必须是函数’)

}

function fBound () {

var restParams = [].slice.call(arguments);

var allParams = boundParams.concat(restParams)

// 通过instanceof判断this是不是fBound的实例

var isConstructor = this instanceof fBound;

if (isConstructor) {

// 如果是,说明是通过new调用的(这里有bug,见下文),那么只要把处理好的参数传给绑定的目标函数,并通过new调用即可。

return new boundTargetFunc(…allParams)

} else {

// 如果不是,说明不是通过new调用的

return boundTargetFunc.apply(boundThis, allParams)

}

}

return fBound

}

最后看了一下MDN提供的bind函数的polyfill,发现思路有点不一样,于是我通过一个实例进行对比。

function test() {}

var fBoundNative = test.bind()

var obj1 = new fBoundNative()

var fBoundMy = test.myBind()

var obj2 = new fBoundMy()

var fBoundMDN = test.mdnBind()

var obj3 = new fBoundMDN()

我发现我的写法看起来竟然更像原生bind。瞬间怀疑自己,但一下子却没找到很明显的bug…

终于我还是意识到了一个很大的问题,obj1fBoundNative的实例,obj3fBoundMDN的实例,但obj2不是fBoundMy的实例(实际上obj2test的实例)。

obj1 instanceof fBoundNative; // true

obj2 instanceof fBoundMy; // false

obj3 instanceof fBoundMDN; // true

存在这个问题麻烦就大了,假设我要在fBoundMy.prototype上继续扩展原型属性或方法,obj2是无法继承它们的。所以最直接有效的方法就是用继承的方法来实现,虽然不能达到原生bind的效果,但已经够用了。于是我参考MDN改了一版。

Function.prototype.myBind = function() {

var boundTargetFunc = this;

if (typeof boundTargetFunc !== ‘function’) {

throw new Error(‘绑定的目标必须是函数’)

}

var boundThis = arguments[0];

var boundParams = [].slice.call(arguments, 1);

function fBound () {

var restParams = [].slice.call(arguments);

var allParams = boundParams.concat(restParams)

return boundTargetFunc.apply(this instanceof fBound ? this : boundThis, allParams)

}

fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)

return fBound

}

这里面最重要的两点:处理好原型链关系,以及理解bind中构造实例的过程

  • 原型链处理

fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)

这一行代码中用了一个||运算符,||的两端充分考虑了myBind函数的两种可能的调用方式。

  1. 常规的函数绑定

function test(name, age) {

this.name = name;

this.age = age;

}

var bound1 = test.myBind(‘小明’)

var obj1 = new bound1(18)

这种情况把fBound.prototype的原型指向boundTargetFunc.prototype,完全符合我们的思维。

  1. 直接使用Function.prototype.myBind

var bound2 = Function.prototype.myBind()

var obj2 = new bound2()

这相当于创建一个新的函数,绑定的目标函数是Function.prototype。这里必然有朋友会问了,Function.prototype也是函数吗?是的,请看!

typeof Function.prototype; // “function”

虽然我还不知道第二种调用方式存在的意义,但是存在即合理,既然存在,我们就支持它。

  • 理解bind中构造实例的过程

首先要清楚new的执行过程,如果您还不清楚这一点,可以看看我写的这篇面试官真的会问:new的实现以及无new实例化。

还是之前那句话,先要判断是不是以构造函数的形式调用的。核心就是这:

this instanceof fBound

我们用一个例子再来分析下new的过程。

function test(name, age) {

this.name = name;

this.age = age;

}

var bound1 = test.myBind(‘小明’)

var obj1 = new bound1(18)

obj1 instanceof bound1 // true

obj1 instanceof test // true

  1. 执行构造函数bound1,实际上是执行myBind执行后返回的新函数fBound。首先会创建一个新对象obj1,并且obj1的非标准属性__proto__指向bound1.prototype,其实就是myBind执行时声明的fBound.prototype,而fBound.prototype的原型指向test.prototype。所以到这里,原型链就串起来了!

  2. 执行的构造函数中,this指向这个obj1

  3. 执行构造函数,由于fBound是没有实际内容的,执行构造函数本质上还是要去执行绑定的那个目标函数,本例中也就是test。因此如果是以构造函数形式调用,我们就把实例对象作为this传给test.apply

  4. 通过执行test,对象实例被挂载了nameage属性,一个崭新的对象就出炉了!

最后附上Raynos大神写的bind实现,我感觉又受到了“暴击”!有兴趣钻研bind终极奥义的朋友请点开链接查看源码!

this指向问题

========

分析this的指向,首先要确定当前执行代码的环境。

全局环境中的this指向


全局环境中,this指向全局对象(视宿主环境而定,浏览器是window,Node是global)。

函数中的this指向


在上文中介绍函数的调用形式时已经比较详细地说过this指向问题了,这里再简单总结一下。

函数中this的指向取决于函数的调用形式,在一些情况下也受到严格模式的影响。

  • 作为普通函数调用:严格模式下,this的值是null,非严格模式下,this指向全局对象。

  • 作为方法调用:this指向所属对象。

  • 作为构造函数调用:this指向实例化的对象。

  • 通过call, apply, bind调用:如果指定了第一个参数thisArgthis的值就是thisArg的值(如果是原始值,会包装为对象);如果不传thisArg,要判断严格模式,严格模式下thisundefined,非严格模式下this指向全局对象。

函数声明和函数表达式

==========

撕了这么久代码,让大脑休息一会儿,先看点轻松点的内容。

函数声明


函数声明是独立的函数语句

function test() {}

函数声明存在提升(Hoisting)现象,如变量提升一般,对于同名的情况,函数声明优于变量声明(前者覆盖后者,我说的是声明阶段哦)。

函数表达式


函数表达式不是独立的函数语句,常作为表达式的一部分,比如赋值表达式。

函数表达式可以是命名的,也可以是匿名的。

// 命名函数表达式

var a = function test() {}

// 匿名函数表达式

var b = function () {}

匿名函数就是没有函数名的函数,它不能单独使用,只能作为表达式的一部分使用。匿名函数常以IIFE(立即执行函数表达式)的形式使用。

(function(){console.log(“我是一个IIFE”)}())

闭包

==

关于闭包,我已经写了一篇超详细的文章去分析了,是个人原创总结的干货,建议直接打开解读闭包,这次从ECMAScript词法环境,执行上下文说起

PS:阅读前,您应该对ECMAScript5的一些术语有一些简单的了解,比如Lexical Environment, Execution Context等。

纯函数

===

  • 纯函数是具备幂等性(对于相同的参数,任何时间执行纯函数都将得到同样的结果),它不会引起副作用。

  • 纯函数与外部的关联应该都来源于函数参数。如果一个函数直接依赖了外部变量,那它就不是纯函数,因为外部变量是可变的,那么纯函数的执行结果就不可控了。

// 纯函数

function pure(a, b) {

return a + b;

}

// 非纯函数

function impure© {

return c + d

}

var d = 10;

pure(1, 2); // 3

impure(1); // 11

d = 20;

impure(1); // 21

pure(1, 2); // 3

惰性函数

====

相信大家在兼容事件监听时,都写过这样的代码。

function addEvent(element, type, handler) {

if (window.addEventListener) {

element.addEventListener(type, handler, false);

} else if (window.attachEvent){

element.attachEvent(‘on’ + type, handler);

} else {

element[‘on’ + type] = handler;

}

}

仔细看下,我们会发现,每次调用addEvent,都会做一次if-else的判断,这样的工作显然是重复的。这个时候就用到惰性函数了。

惰性函数表示函数执行的分支只会在函数第一次调用的时候执行。后续我们所使用的就是这个函数执行的结果。

利用惰性函数的思维,我们可以改造下上述代码。

function addEvent(element, type, handler) {

if (window.addEventListener) {

addEvent = function(element, type, handler) {

element.addEventListener(type, handler, false);

}

} else if (window.attachEvent){

addEvent = function(element, type, handler) {

element.attachEvent(‘on’ + type, handler);

}

} else {

addEvent = function(element, type, handler) {

element[‘on’ + type] = handler;

}

}

addEvent(type, element, fun);

}

这代码看起来有点low,但是它确实减少了重复的判断。在这种方式下,函数第一次执行时才确定真正的值。

我们还可以利用IIFE提前确定函数真正的值。

var addEvent = (function() {

if (window.addEventListener) {

return function(element, type, handler) {

element.addEventListener(type, handler, false);

}

} else if (window.attachEvent){

return function(element, type, handler) {

element.attachEvent(‘on’ + type, handler);

}

} else {

return function(element, type, handler) {

element[‘on’ + type] = handler;

}

}

}())

高阶函数

====

函数在javascript中是一等公民,函数可以作为参数传给其他函数,这让函数的使用充满了各种可能性。

不如来看看维基百科中高阶函数(High-Order Function)的定义:

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  1. 接受一个或多个函数作为输入
  1. 输出一个函数

看到这,大家应该都意识到了,平时使用过很多高阶函数。数组的一些高阶函数使用得尤为频繁。

[1, 2, 3, 4].forEach(function(item, index, arr) {

console.log(item, index, arr)

})

[1, 2, 3, 4].map(item => 小老弟${item})

可以发现,传入forEachmap的就是一个函数。我们自己也可以封装一些复用的高阶函数。

我们知道Math.max可以求出参数列表中最大的值。然而很多时候,我们需要处理的数据并不是1, 2, 3, 4这么简单,而是对象数组。

假设有这么一个需求,存在一个数组,数组元素都是表示人的对象,我们想从数组中选出年纪最大的人。

这个时候,就需要一个高阶函数来完成。

/**

* 根据求值条件判断数组中最大的项

* @param {Array} arr 数组

* @param {String|Function} iteratee 返回一个求值表达式,可以根据对象属性的值求出最大项,比如item.age。也可以通过自定义函数返回求值表达式。

*/

function maxBy(arr, iteratee) {

let values = [];

if (typeof iteratee === ‘string’) {

values = arr.map(item => item[iteratee]);

} else if (typeof iteratee === ‘function’) {

values = arr.map((item, index) => {

return iteratee(item, index, arr);

});

}

const maxOne = Math.max(…values);

const maxIndex = values.findIndex(item => item === maxOne);

return arr[maxIndex];

}

利用这个高阶函数,我们就可以求出数组中年纪最大的那个人。

var list = [

{name: ‘小明’, age: 18},

{name: ‘小红’, age: 19},

web浏览器中的javascript

window对象

  • 计时器

  • 浏览器定位和导航

  • 浏览历史

  • 浏览器和屏幕信息

  • 对话框

  • 错误处理

  • 作为window对象属性的文档元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值