如果您需要换个角度看闭包,请直接打开解读闭包,这次从ECMAScript词法环境,执行上下文说起。
本文总结了javascript中函数的常见知识点,包含了基础概念,闭包,this指向问题,高阶函数,柯里化等,手写代码那部分也是满满的干货,无论您是想复习准备面试,还是想深入了解原理,本文都应该有你想看的点,总之还是值得一看的。
老规矩,先上思维导图。
什么是函数
=====
一般来说,一个函数是可以通过外部代码调用的一个“子程序”(或在递归的情况下由内部函数调用)。像程序本身一样,一个函数由称为函数体的一系列语句组成。值可以传递给一个函数,函数将返回一个值。
函数首先是一个对象,并且在javascript中,函数是一等对象(first-class object)。函数可以被执行(callable,拥有内部属性[[Call]]),这是函数的本质特性。除此之外,函数可以赋值给变量,也可以作为函数参数,还可以作为另一个函数的返回值。
函数基本概念
======
函数名
函数名是函数的标识,如果一个函数不是匿名函数,它应该被赋予函数名。
-
函数命名需要符合javascript标识符规则,必须以字母、下划线_或美元符$开始,后面可以跟数字,字母,下划线,美元符。
-
函数命名不能使用javascript保留字,保留字是javascript中具有特殊含义的标识符。
-
函数命名应该语义化,尽量采用动宾结构,小驼峰写法,比如
getUserName()
,validateForm()
,isValidMobilePhone()
。 -
对于构造函数,我们通常写成大驼峰格式(因为构造函数与类的概念强关联)。
下面是一些不成文的约定,不成文代表它不必遵守,但是我们按照这样的约定来执行,会让开发变得更有效率。
-
__xxx__
代表非标准的方法。 -
_xxx
代表私有方法。
函数参数
形参
形参是函数定义时约定的参数列表,由一对圆括号()
包裹。
在MDN上有看到,一个函数最多可以有255
个参数。
然而形参太多时,使用者总是容易在引用时出错。所以对于数量较多的形参,一般推荐把所有参数作为属性或方法整合到一个对象中,各个参数作为这个对象的属性或方法来使用。举个例子,微信小程序的提供的API基本上是这种调用形式。
wx.redirectTo(Object object)
调用示例如下:
wx.redirectTo({
url: ‘/article/detail?id=1’,
success: function() {},
fail: function() {}
})
形参的数量可以由函数的length
属性获得,如下所示。
function test(a, b, c) {}
test.length; // 3
实参
实参是调用函数时传入的,实参的值在函数执行前被确定。
javascript在函数定义时并不会约定参数的数据类型。如果你期望函数调用时传入正确的数据类型,你必须在函数体中对入参进行数据类型判断。
function add(a, b) {
if (typeof a !== ‘number’ || typeof b !== ‘number’) {
throw new Error(“参数必须是数字类型”)
}
}
好在Typescript提供了数据类型检查的能力,这一定程度上防止了意外情况的发生。
实参的数量可以通过函数中arguments
对象的length
属性获得,如下所示。
实参数量不一定与形参数量一致。
function test(a, b, c) {
var argLength = arguments.length;
return argLength;
}
test(1, 2); // 2
默认参数
函数参数的默认值是undefined
,如果你不传入实参,那么实际上在函数执行过程中,相应参数的值是undefined
。
ES6也支持在函数声明时设置参数的默认值。
function add(a, b = 2) {
return a + b;
}
add(1); // 3
在上面的add
函数中,参数b
被指定了默认值2
。所以,即便你不传第二个参数b
,也能得到一个预期的结果。
假设一个函数有多个参数,我们希望不给中间的某个参数传值,那么这个参数值必须显示地指定为undefined
,否则我们期望传给后面的参数的值会被传到中间的这个参数。
function printUserInfo(name, age = 18, gender) {
console.log(姓名:${name},年龄:${age},性别:${gender}
);
}
// 正确地使用
printUserInfo(‘Bob’, undefined, ‘male’);
// 错误,'male’被错误地传给了age参数
printUserInfo(‘Bob’, ‘male’);
PS:注意,如果你希望使用参数的默认值,请一定传undefined
,而不是null
。
当然,我们也可以在函数体中判断参数的数据类型,防止参数被误用。
function printUserInfo(name, age = 18, gender) {
if (typeof arguments[1] === ‘string’) {
age = 18;
gender = arguments[1];
}
console.log(姓名:${name},年龄:${age},性别:${gender}
);
}
printUserInfo(‘bob’, ‘male’); // 姓名:bob,年龄:18,性别:male
这样一来,函数的逻辑也不会乱。
剩余参数
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
剩余参数通过剩余语法...
将多个参数聚合成一个数组。
function add(a, …args) {
return args.reduce((prev, curr) => {
return prev + curr
}, a)
}
剩余参数和arguments
对象之间的区别主要有三个:
-
剩余参数只包含那些没有对应形参的实参,而
arguments
对象包含了传给函数的所有实参。 -
arguments
对象不是一个真正的数组,而剩余参数是真正的Array
实例,也就是说你能够在它上面直接使用所有的数组方法,比如sort
,map
,forEach
或pop
。而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
也是类数组,arguments
的length
由实参的数量决定,而不是由形参的数量决定。
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.caller
和arguments.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
关键字使用时就成了构造函数。构造函数用于实例化对象,构造函数的执行过程大致如下:
-
首先创建一个新对象,这个新对象的
__proto__
属性指向构造函数的prototype
属性。 -
此时构造函数的
this
指向这个新对象。 -
执行构造函数中的代码,一般是通过
this
给新对象添加新的成员属性或方法。 -
最后返回这个新对象。
实例化对象也可以通过一些技巧来简化,比如在构造函数中显示地return
另一个对象,jQuery很巧妙地利用了这一点。具体分析详见面试官真的会问:new的实现以及无new实例化。
通过call, apply调用
apply
和call
是函数对象的原型方法,挂载于Function.prototype
。利用这两个方法,我们可以显示地绑定一个this
作为调用上下文,同时也可以设置函数调用时的参数。
apply
和call
的区别在于:提供参数的形式不同,apply
方法接受的是一个参数数组,call
方法接受的是参数列表。
someFunc.call(obj, 1, 2, 3)
someFunc.apply(obj, [1, 2, 3])
注意,在非严格模式下使用call
或者apply
时,如果第一个参数被指定为null
或undefined
,那么函数执行时的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
=================
call
,apply
,bind
都可以绑定this
,区别在于:apply
和call
是绑定this
后直接调用该函数,而bind
会返回一个新的函数,并不直接调用,可以由程序员决定调用的时机。
bind
的语法形式如下:
function.bind(thisArg[, arg1[, arg2[, …]]])
bind
的arg1, arg2, ...
是给新函数预置好的参数(预置参数是可选的)。当然新函数在执行时也可以继续追加参数。
手写call, apply, bind
===================
提到call
,apply
,bind
总是无法避免手写代码这个话题。手写代码不仅仅是为了应付面试,也是帮助我们理清思路和深入原理的一个好方法。手写代码一定不要抄袭,如果实在没思路,可以参考下别人的代码整理出思路,再自己按照思路独立写一遍代码,然后验证看看有没有缺陷,这样才能有所收获,否则忘得很快,只能短时间应付应付。
那么如何才能顺利地手写代码呢?首先是要清楚一段代码的作用,可以从官方对于它的定义和描述入手,同时还要注意一些特殊情况下的处理。
就拿call
来说,call
是函数对象的原型方法,它的作用是绑定this
和参数,并执行函数。调用形式如下:
function.call(thisArg, arg1, arg2, …)
那么我们慢慢来实现它,将我们要实现的函数命名为myCall
。首先myCall
是一个函数,接受的第一个参数thisArg
是目标函数执行时的this
的值,从第二个可选参数arg1
开始的其他参数将作为目标函数执行时的实参。
这里面有很多细节要考虑,我大致罗列了一下:
-
要考虑是不是严格模式。如果是非严格模式,对于
thisArg
要特殊处理。 -
如何判断严格模式?
-
thisArg
被处理后还要进行非空判断,然后考虑是以方法的形式调用还是以普通函数的形式调用。 -
目标函数作为方法调用时,如何不覆盖对象的原有属性?
实现代码如下,请仔细看我写的注释,这是主要的思路!
// 首先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
了,首先要明确,bind
与call
, 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…
终于我还是意识到了一个很大的问题,obj1
是fBoundNative
的实例,obj3
是fBoundMDN
的实例,但obj2
不是fBoundMy
的实例(实际上obj2
是test
的实例)。
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
函数的两种可能的调用方式。
- 常规的函数绑定
function test(name, age) {
this.name = name;
this.age = age;
}
var bound1 = test.myBind(‘小明’)
var obj1 = new bound1(18)
这种情况把fBound.prototype
的原型指向boundTargetFunc.prototype
,完全符合我们的思维。
- 直接使用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
-
执行构造函数
bound1
,实际上是执行myBind
执行后返回的新函数fBound
。首先会创建一个新对象obj1
,并且obj1
的非标准属性__proto__
指向bound1.prototype
,其实就是myBind
执行时声明的fBound.prototype
,而fBound.prototype
的原型指向test.prototype
。所以到这里,原型链就串起来了! -
执行的构造函数中,
this
指向这个obj1
。 -
执行构造函数,由于
fBound
是没有实际内容的,执行构造函数本质上还是要去执行绑定的那个目标函数,本例中也就是test
。因此如果是以构造函数形式调用,我们就把实例对象作为this
传给test.apply
。 -
通过执行
test
,对象实例被挂载了name
和age
属性,一个崭新的对象就出炉了!
最后附上Raynos大神写的bind实现,我感觉又受到了“暴击”!有兴趣钻研bind
终极奥义的朋友请点开链接查看源码!
this指向问题
========
分析this
的指向,首先要确定当前执行代码的环境。
全局环境中的this指向
全局环境中,this指向全局对象(视宿主环境而定,浏览器是window,Node是global)。
函数中的this指向
在上文中介绍函数的调用形式时已经比较详细地说过this
指向问题了,这里再简单总结一下。
函数中this
的指向取决于函数的调用形式,在一些情况下也受到严格模式的影响。
-
作为普通函数调用:严格模式下,
this
的值是null
,非严格模式下,this
指向全局对象。 -
作为方法调用:
this
指向所属对象。 -
作为构造函数调用:
this
指向实例化的对象。 -
通过call, apply, bind调用:如果指定了第一个参数
thisArg
,this
的值就是thisArg
的值(如果是原始值,会包装为对象);如果不传thisArg
,要判断严格模式,严格模式下this
是undefined
,非严格模式下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) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
完整版面试题资料免费分享,只需你点赞支持,动动手指点击此处就可免费领取了。
回顾项目
往往在面试时,面试官根据你简历中的项目由点及面地展开问答,所以请对你做过的最好的项目进行回顾和反思。回顾你做过的工作和项目中最复杂的部分,反思你是如何完成这个最复杂的部分的。
面试官会重点问你最复杂的部分的实现方法和如何优化。重点要思考如何优化,即使你项目中没有对那部分进行优化,你也应该预先思考有什么优化的方案。如果这部分答好了,会给面试官留下很不错的印象。
重点在于基础知识
这里指的基础知识包括:前端基础知识和学科基础知识。
前端基础知识:html/css/js 的核心知识,其中 js 的核心知识尤为重要。比如执行上下文、变量对象/活动对象(VO/AO)、作用域链、this 指向、原型链等。
学科基础知识:数据结构、计算机网络、算法等知识。你可能会想前端不需要算法,那你可能就错了,在大公司面试,面试官同样会看重学生这些学科基础知识。
你可能发现了我没有提到React/Vue
这些框架的知识,这里得说一说,大公司不会过度的关注这方面框架的知识,他们往往更加考察学生的基础。
这里我的建议是,如果你至少使用或掌握其中一门框架,那是最好的,可以去刷刷相关框架的面试题,这样在面试过程中即使被问到了,也可以回答个 7788。如果你没有使用过框架,那也不需要太担心,把重点放在基础知识和学科基础知识之上,有其余精力的话可以去看看主流框架的核心思想。
ment, type, handler) {
element.addEventListener(type, handler, false);
}
} else if (window.attachEvent){
addEvent = function(element, type, handler) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-cBWQuqyS-1713783647535)]
[外链图片转存中…(img-IMvOM9q0-1713783647536)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-G6MwAvkt-1713783647536)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
[外链图片转存中…(img-K8yeFLvc-1713783647536)]
完整版面试题资料免费分享,只需你点赞支持,动动手指点击此处就可免费领取了。
回顾项目
往往在面试时,面试官根据你简历中的项目由点及面地展开问答,所以请对你做过的最好的项目进行回顾和反思。回顾你做过的工作和项目中最复杂的部分,反思你是如何完成这个最复杂的部分的。
面试官会重点问你最复杂的部分的实现方法和如何优化。重点要思考如何优化,即使你项目中没有对那部分进行优化,你也应该预先思考有什么优化的方案。如果这部分答好了,会给面试官留下很不错的印象。
重点在于基础知识
这里指的基础知识包括:前端基础知识和学科基础知识。
前端基础知识:html/css/js 的核心知识,其中 js 的核心知识尤为重要。比如执行上下文、变量对象/活动对象(VO/AO)、作用域链、this 指向、原型链等。
学科基础知识:数据结构、计算机网络、算法等知识。你可能会想前端不需要算法,那你可能就错了,在大公司面试,面试官同样会看重学生这些学科基础知识。
你可能发现了我没有提到React/Vue
这些框架的知识,这里得说一说,大公司不会过度的关注这方面框架的知识,他们往往更加考察学生的基础。
这里我的建议是,如果你至少使用或掌握其中一门框架,那是最好的,可以去刷刷相关框架的面试题,这样在面试过程中即使被问到了,也可以回答个 7788。如果你没有使用过框架,那也不需要太担心,把重点放在基础知识和学科基础知识之上,有其余精力的话可以去看看主流框架的核心思想。