函数闭包与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
则为形参的长度。