logo头像
Snippet 博客主题

函数和闭包

参考链接

函数(MDN)
闭包(MDN)
JavaScript深入之词法作用域和动态作用域
JavaScript深入之执行上下文
JavaScript深入之闭包
JavaScript深入之从ECMAScript规范解读this

函数

原始参数(比如一个具体的数字)被作为值传递给函数;值被传递给函数,如果被调用函数改变了这个参数的值,这样的改变不会影响到全局或调用函数。

1
2
3
function square(number) {
return number * number;
}

如果你传递一个对象(即一个非原始值,例如Array或用户自定义的对象)作为参数,而函数改变了这个对象的属性,这样的改变对函数外部是可见的,如下面的例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
function myFunc(theObject) {
theObject.make = "Toyota";
}

var mycar = {make: "Honda", model: "Accord", year: 1998};
var x, y;

x = mycar.make; // x获取的值为 "Honda"

myFunc(mycar);
y = mycar.make; // y获取的值为 "Toyota"
// (make属性被函数改变了)

在 JavaScript 中,可以根据条件来定义一个函数。比如下面的代码,当num 等于 0 的时候才会定义 myFunc :

1
2
3
4
5
6
var myFunc;
if (num == 0){
myFunc = function(theObject) {
theObject.make = "Toyota"
}
}

函数一定要处于调用它们的域中,但是函数的声明可以被提升(出现在调用语句之后),如下例:

1
2
3
console.log(square(5));
/* ... */
function square(n) { return n*n }

函数域是指函数声明时的所在的地方,或者函数在顶层被声明时指整个程序。

注意只有使用如上的语法形式(即function funcName(){} 才可以。而下面的代码是无效的。就是说,函数提升仅适用于函数声明,而不适用于函数表达式

1
2
3
4
5
console.log(square); // square is hoisted with an initial value undefined.
console.log(square(5)); // TypeError: square is not a function
var square = function (n) {
return n * n;
}

作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var value = 1;

function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

bar();

// 结果是 ???

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

this

如何正确判断 this?箭头函数的 this 是什么?

this 是很多人会混淆的概念,但是其实它一点都不难,只是网上很多文章把简单的东西说复杂了。在这一小节中,你一定会彻底明白 this 这个概念的。

我们先来看几个函数调用的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log(this.a)
}

var a = 1
foo()

const obj = {
a: 2,
foo: foo
}
obj.foo()

const c = new foo()
  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo函数中的 this 就是 obj 对象
  • 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this

说完了以上几种情况,其实很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

1
2
3
4
5
6
7
8
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。

另外对箭头函数使用 bind 这类函数是无效的。

最后种情况也就是 bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window

那么说到 bind,不知道大家是否考虑过,如果对一个函数进行多次 bind,那么上下文会是什么呢?

1
2
3
let a = {} 
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

如果你认为输出结果是 a,那么你就错了,其实我们可以把上述代码转换成另一种形式

1
2
3
4
5
6
7
// fn.bind().bind(a) 等于 
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()

以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

如果你还是觉得有点绕,那么就看以下的这张流程图吧,图中的流程只针对于单个规则。

679bc918fa600cb15047398ca21eed30.png

闭包

什么是闭包?

闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

1
2
3
4
5
6
7
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

经典面试题,循环中使用闭包解决 var 定义函数的问题

1
2
3
4
5
for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

解决办法有三种,第一种是使用闭包的方式

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}

第二种就是使用 setTimeout 的第三个参数

1
2
3
4
5
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}

第三种就是使用 let 定义 i 了

1
2
3
4
5
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

因为对于 let 来说,他会创建一个块级作用域,相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}