函数闭包与this指针
# 0.前言
本篇博客涉及的主要内容涉及 函数调用 以及在javaScript 中难以理解的 this 。
# 1. 函数的定义
一段程序一般由三个部分构成:
- 函数:有返回值。(在
JavaScript中所有的函数都有返回值) - 过程:无返回值。
- 方法:即
method,封装在类或者对象中。
上述中的"函数"这个定义需要区别于传统意义上数学函数表达式的概念。在数学中一个函数表达式输入和输出具有明确的关系,即一旦确定自变量x的数组,则因变量y明确。而在 JavaScript 函数代码中,一个函数可以没有明确的输入输出关系,如下:
let x = 0;
function add(){
x += 1
return x
}
add() // 1
add() // 2
2
3
4
5
6
7
8
当然在设计的时候,我们可以强制保证函数具有明确的输入输出关系,我们将满足此关系的函数代码称为纯函数 代码,将代码内部破坏其 纯函数 特性的行为,称为 副作用 。
# 2.函数的返回值由什么决定?
一段函数代码的返回值是由:调用时的param 以及定义时的env 决定的,见下面这段代码:
let x = "x";
let a = "1"; // 定义时的 env
function f1(x){
return x + a // 由f1函数定义可知,a是环境(env)参数,x是输入(param)参数
};
// 块级作用域
{
let a = "2";
console.log(f1("x")); // 调用时的 param
}
2
3
4
5
6
7
8
9
10
11
最终答案为:"x1"
其中, "x" 是调用时的 param,而 a 是定义时的 env。
一般面试题中的难点在于:定义时的 env 不容易确定,并且需要注意的是,f1 在定义时维持住的是 env 环境变量,如果当变量发生改变时,所对应的值也发生变化。如下:
let x = "x";
let a = "1";
function f1(x){
return x + a
};
let a = "3";
// 块级作用域
{
let a = "2";
console.log(f1("x")); // 答案: "x3"
}
let a = "4"
2
3
4
5
6
7
8
9
10
11
12
13
# 3.闭包
接着上述的案例,正好可以引入到 "闭包" 知识点。
上述函数的 output 取决于两个部分,调用时的param 参数以及 env 参数 。前者基本上我们不会判断出错,问题就在于env 参数。其实在函数定义时,就已将输出"锚定"在定义作用域中了(即,env 环境参数)。
而闭包的核心目的也是如此:使用函数 维护 变量。
最典型的闭包是:
for(var i=0;i<6;i++){
setTimeout(()=>{console.log(i)},0)
}
// 打印 6 个 6
2
3
4
原因是,setTimeout 此函数中的 i 统一取决于其定义环境中的 i 变量,又闭包维护的是 i 变量,而非变量值,因此当值发生变化时,取决于最后一个i 对应的数值。
改进方式:除了传统的将 var 变量改为let 的方案外,我们还可以使用立即执行函数 将env 参数变为param 参数。
for (var i = 0; i < 6; i++) {
!function(i){
setTimeout(()=>{
console.log(i)
})
}(i)
}
// 打印 0 1 2 3 4 5
2
3
4
5
6
7
8
# 4.闭包的高级理解及应用
由上述已知,闭包的目的就在于维护一个外部变量(env 参数)。使用 对象 也完全可以实现此效果:
var obj = {
_i: 0,
fn(){
console.log(_i)
}
}
2
3
4
5
6
将其改写为 函数 形式,即为 ”闭包“ :
const handle = function(){
var i = 0;
return function(){
console.log(i)
}
}
2
3
4
5
6
以上,属于 闭包 的基础版本,在实际的案例中,其实闭包使用的非常频繁,如:
在手写图片懒加载中,使用
闭包,维护一个count变量统计当前页面img标签。在节流与防抖函数中,使用
闭包,维护timer引用地址,或者cur\past等变量。function debounce(fn,delay){ let timer = null; return function(){....} }1
2
3
4在
promise.all中,使用闭包,维护一个count变量统计resolved成功的个数,以及result数组。在
once函数中,使用闭包,在外部缓存维护:flag和result数组。function once(fn){ let revoked = false; return (...args)=>{ if(revoked) return result; revoked = true; return fn(...args); } }1
2
3
4
5
6
7
8
# 5.this 指针
在 JavaScript 中最难理解的部分,也即 this 指针的指向问题。
在下面的函数可以发现,this 等价于env 变量,即箭头函数式如何处理 a 的,就如何处理 this 指针。
// 代码1
const a = 233
const fn2 = ()=>{console.log(a)}
// 代码2:
console.log(this)
const fn2 = ()=>{console.log(this)}
2
3
4
5
6
7
在 JavaScript 中,可以使用 call 、apply 以及 bind 对函数中的 this 进行修改。
网上中有关this 指向的口诀特别多,其实只需要理解下面三种情况下的 this 指针,即可做到以不变应万变:
fn(1,2)
// fn.call(undefined,1,2); Node 环境
obj.method("hi")
// obj.method.call(obj,"hi")
array[0]("hi")
// array[0].call(array,"hi")
2
3
4
5
6
上述注释部分,实际上做了一层 call 改写,将 this 进行显式改写。
注:第1个例子中,直接触发
fn时,this传入的是当前的全局环境,在Node中全局环境是undefined,在Chrome中全局环境默认是windows。
还需要注意的是,一定要区分函数的执行时的调用对象,如下,在编写事件触发函数时,会写以下代码:
button.onclick = function(e){
console.log(this)
}
2
3
此时需要注意的是,上述函数是由用户点击触发的,还是引用触发的。
如果是用户点击触发,即button.onclick(),此时 this 即为 button 对象。
而若是由引用触发,则需要继续讨论:
let obj = {}
obj.f = button.onclick;
obj.f() // 则此时 this 为 obj 对象
2
3
# 6.面试题实战
经过上面对函数、闭包以及指针的讲解后,来做一道比较奇葩的面试题。
let length = 10;
function fn(){console.log(this.length)}
let obj = {
length: 5,
method(fn){
fn();
arguments[0]();
}
}
obj.method(fn,1)
2
3
4
5
6
7
8
9
10
11
12
在 Node 端的运行结果:
undefined
2
2
在 Chrome 端运行结果为:
取决于当前页面的 iframe 个数
2
2
只需将执行时的function 改写后,即可得到结果:
let obj = {
length: 5,
method(fn){
fn(); // fn.call(undefined|windows)
arguments[0](); // arguments[0].call(arguments)
}
}
2
3
4
5
6
7
经改写后,fn() 被改写为fn.call(undefined|windows) (在Node 环境中,this传入 undefined,在 Chrome 中,this 传入 windows)
看下 Chrome 在执行阶段的代码:
let length = 10;
funtion(){
console.log(windows.length);
};
2
3
4
注意到上述使用到的定义是 let ,而非var。而前者不会将变量挂载到 windows 对象,而默认指的是当前浏览器的 iframe 标签个数。
arguments[0]() 函数则被解析为arguments[0].call(arguments) 表达式,而此时arguments 为fn 和 1 两个输入参数,此时this.length 则为形参的长度。