JavaScript 闭包

1 使用场景

  自己写demo的时候碰到一个问题,我在使用document.addEventListener()的时候,添加的callback必须要传入几个参数。这个问题不大,我可以用闭包的形式这样写

1
2
3
4
5
6
7
const listener = (arg1,arg2) =>{
return (e) => {
console.log(e)
console.log(arg2)
console.log(arg1)
}
}

但是我用的react我需要在componentWillUnMount的时候需要这个eventListener删了,这种返回的function 是个匿名函数是删不掉的。
  这个时候就需要bind()方法了,bind()方法创建一个新的函数,新函数的this,被方法的第一个参数指定,其余参数则作为新函数的参数提供调用。咱可以这样写

1
2
3
4
5
6
7
8
9
const listener = (arg1,arg2,e) => {
console.log(arg1);
console.log(arg2);
console.log(e);
}

const keyListener = listener.bind(null,arg1,arg2);
document.addListener("keydown",keyListener);
document.removeEventListener('keydown', keyListener);

2 引申及思考

  好了,这里涉及到的代码可以引申出去的知识点有下面这几个:this, bind(), call(), apply(), closure。this,bind(),closure不提,为啥会扯到call() apply()呢,因为mdn翻了一下function类型的prototype方法除了toString()就三个,bind(),call(),apply(),顺便了解下没毛病。

2.1 闭包

  咱按照《你不知道的JavaScript》的顺序来讲,先讲讲闭包。闭包的外在表现是在定义时的词法作用域外的地方被调用时可以访问定义时的作用域。比如这里的

1
2
3
4
5
6
7
8
const listener = (arg1,arg2) =>{
return (e) => {
console.log(e)
console.log(arg2)
console.log(arg1)
}
}
document.addListener("keydown",listener());

  当我们keydown事件触发的时候我具体执行的是这样一个匿名函数

1
(e)=>{ console.log(e);console.log(arg2);console.log(arg1);  }

  其中e是事件触发时callback传入的参数,而arg2和arg1的值完全是函数定义的时候的变量….闭包是个啥东西呢,基本可以认为如果将具体执行函数单独挑出来,发现其中调用了定义时的作用域的变量,那么这就是个闭包….所以其实书中认为IIFE并不是个闭包..
  关于闭包我再翻个var带来的闭包老黄历,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<ul>
<li>adf</li>
<li>d</li>
<li>f</li>
<li>fff</li>
<li>zc</li>
<li>ad</li>
<li>vv</li>

</ul>
</body>
<script>
var list = document.getElementsByTagName("li");
console.log(list);
for (var i = 0; i < list.length; i++) {
list[i].addEventListener("click", () => {
console.log(i)
})
}
</script>

  这个时候发现无论点的哪个,console出来的都是7,原因是当我们点击调用callback的时候,函数去定义时作用域查找变量i,但是这个时候已经循环结束,i的值一直都是7,所以每次click都是打印的7。以前正确做法是在addEventListener处自己创建一个闭包来存储每次的变量副本

1
2
3
4
5
6
7
8
for (var i = 0; i < list.length; i++) {
(function () {
var j = i;
list[i].addEventListener("click", () => {
console.log(j)
});
})();
}

  现在嘛,只要for的时候i用let关键字来定义就好了….事实告诉我再用var我就是傻逼。(再吐个槽,var没有块级作用域真的是个傻逼东西)

2.2 this

  关于this,js的this指向要判断runtime的调用环境,然后再根据代码几种绑定类型判断具体this是个啥。(像我这种java写习惯的一开始碰到这个this真的是蒙了)。

  1. 调用环境在function中
    如果是代码处于严格模式下严格模式则this是undefined,如果不是则指向window(这里有个很坑爹的情况,就是一块代码中严格模式和非严格模式混用….最终this的指向取决于写的this的位置是否处于严格模式代码块中…虽然我觉得我可能用不到这个知识点..)
  2. 调用环境在object中
    调用环境在object中this指向的就是object…书上还特意给了函数作为callback时以及函数以别名形式被调用时的情况,说了一堆this丢失…..其实都没必要,反正最终看的还是函数执行时环境即可…..针对这两种情况下面这个例子就很好理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log('in function');
console.log(this);// should be ’window‘
}

function obj() {
console.log('in object');
console.log(this);//should be 'obj2'
foo();
}
var obj2 = {
child: obj,
b: 3
}
obj2.child()
  1. 使用function.prototype方法手动绑定
    这个方法就是js提供了api让自己手动绑定this值呗,就是call apply bind 三个方法,具体这三个方法有啥用,MDN有,后面我也会有记录。
  2. new 操作符
    js 的new操作符是个很神奇的东西,因为它new出来的东西永远是个object…所以new操作符下的所有this指向的都是new出来的object,例子如下
1
2
3
4
function foo() {
console.log(this);// 打印出来的是一个名为foo的object...
}
var a = new foo();

  这里还要着重说明下arrow function的this,指向的是外层环境的this而不是根据runtime环境判断this,这很关键,因为一般function需要看执行栈判断,但是arrow function则只需要看你代码的环境。所以箭头函数的开销比一般的function大,如果需要极致优化,建议能用function定义就不要用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
setTimeout(() => {
// 箭头函数的this指向foo
console.log(this.a);
}, 100);
}
var obj = {
a: 2
};
foo.call(obj); // 2

function foo() {
setTimeout(function(){
// 一般函数的this要根据调用环境判断,所以这里timeout的callback的this是window
console.log(this);
}, 100);
}
var obj = {
a: 2
};
foo.call(obj); // 2

3 bind call apply

  终于是扯到这三个函数了,其实最开始写react的时候发现react在render里面的jsx的onclick绑定component方法有两种写法,但大致就是一种用arrow function一个是在constructor的时候用bind方法。当时云里雾里的,现在看了看js里面的这些概念终于是明白为啥arrow function能用了。click事件的执行环境肯定不在原组件上啦,那这个click的this就应该是根据最终callback执行时环境判定(仔细搜了一下,react的事件其实有一套自己的事件合成,所以其实jsx 里面的 onclick应该是指向最终调用的component的..好吧不是很确定)。那arrow function是可以将this指向代码书写环境,那就可以直接调用component里面的函数,那么bind是个什么吊东西…..

  1. bind()
    bind创建返回一个新的函数(继承原函数),其中bind的第一个参数会作为新函数的this指向,其余参数则会作为参数传入新函数。那其实在constructor里面执行bind只是单纯的将当前component绑定到目标方法的this上面….为什么会和最终的callback的this绑一起…这就很费解了。这个问题暂时留去给以后想,现在我们专注于这三个function。
  2. call apply
    call apply 都是给函数指定this和参数并调用(bind是返回包装过的函数),两者唯一区别是apply的参数是作为一个数组apply(this,[argArray])。原函数收到的是一个数组,call则是传入一个参数列表call(this,args1,args2….)。原函数收到的是一个个参数。

最后呢,再提一个面试可能问道的问题,也比较现实的问题,如何实现bind(),因为bind()是es5标准,要兼容的话咱得加polyfill,其实MDN上面有来着,而且也推荐了github上面一个实现bind的lib:function-bind。要我自己写的话…其实基本就是用一下call和apply…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.bind = function (originThis) {

var args = Array.prototype.slice.call(arguments, 1);
//这里的这个this就是需要bind的function
var functionToBind = this;
var functionBound = function () {
// this instanceof functionBound === true时,说明返回的fBound被当做new的构造函数调用
//new 的优先级大于bind,所以这里的this,应该是默认的this
var finalThis = this instanceof functionBound ? this : originThis;

return functionToBind .apply(finalThis , args.concat(Array.prototype.slice.call( arguments)));
};

return functionBound ;
};