理解JavaScript闭包
作用域链
每个函数都有自己的执行环境,全局执行环境是最外围的一个执行环境。每个执行环境都有一个与之关联的变量对象,这个变量对象保存了该环境定义的所有变量和函数。当代码在一个环境中执行时便会创建变量对象的一个作用域链,保证可以有序访问执行环境的所有变量和函数。在之前学习Java语法的时候,当函数返回时函数内部定义的局部变量就不存在了,这是因为这些局部变量定义在CPU的栈中。而JavaScript将作用域链描述为一个对象列表(注:可以理解为Java里面的对象,存储在堆中,只要有指向该对象的引用,这个对象就不会被垃圾回收)。
在定义一个函数的时候,它的作用域链实际上只保存了全局变量对象;在调用这个函数时,它才创建一个新的变量对象来存储它的局部变量以及嵌套的函数,并将这个对象添加到作用域链上。对于嵌套函数,实际上每次调用外部函数时,内部函数都会被重新定义一遍。
function compare (value1, value2){ if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } } var result = compare(5, 10);
当JavaScript需要查找某个变量时,它会从Scope Chain中按索引序号在每一个变量对象中查找,若最后不存在该变量则抛出一个引用错误(ReferenceError)异常。
闭包
理解了JavaScript函数的作用域链,我们就可以理解“闭包”的作用了。作用域链的本质是一个指向变量对象的指针列表,它本身并不包含变量对象。若某个函数内部不包含其他嵌套函数,那么当该函数返回时,就会从作用域链中将这个定义了局部变量的变量对象删除。但如果定义了嵌套函数,并将它作为返回值返回或存储在某处的属性里,这个就会有一个外部引用指向这个嵌套函数,这个嵌套函数有它自己的作用域链(而对于“对象”来说,只要有引用指向它,它就不会被垃圾回收)。因此,即使外部函数返回了,这个外部函数里面定义的嵌套函数依然运行在它自身的作用域链中。对比代码1和2:
代码1:
var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() {return scope;} return f(); } checkscope(); //输出"local scope"
代码2:
var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() {return scope;} return f; } checkscope()(); //仍然输出"local scope"
相比于代码1,代码2中函数checkscope返回的是内部函数f的引用,而不是f的返回值。这样我们可能会有一个误区:代码2中函数checkscope返回的是函数f的代码,而后面再接一个小括号()对函数f进行立即调用,那么执行环境为全局变量对象时应该返回的是"global scope"。其实不然,因为外部函数checkscope在定义时就已经将它的变量对象绑定到内部函数f的作用域链上了,而函数定义时的作用域链直到函数执行时依然有效,那么在函数f的作用域链中,用下往上搜索,搜索变量scope始终是外部函数定义的scope。
可以这么理解:checkscope()();语句中第一个小括号是调用函数checkscope,返回函数f的引用,此时函数checkscope已经返回,但它内部定义的局部变量及函数仍然有效,因为所返回的函数f的作用域链仍指向原先checkscope的作用域链(只要有指向对象的引用,该对象就不会被垃圾回收)。而第二个小括号是调用函数f,返回值是经由作用域链搜索得到的变量scope的值。由此可以看出,“闭包”的作用就是可以捕捉到局部变量(和参数)并一直保存下来。
利用闭包实现自定义bind()
Function.prototype.bind():参数列表(obj[, arg1[, arg2[, ...]]])
该方法的作用是将返回的一个新函数绑定到一个对象obj中,使得新函数的this指向参数中的对象obj。当目标函数被调用时,arg1,arg2,...是被预置入绑定函数的参数列表中的参数。
我们可以调用apply()来实现自定义mybind()函数,示例如下:
Function.prototype.mybind = function (obj){ let fn = this; //this指向调用绑定函数的原函数 let bounceArgs = arguments; //保存调用绑定函数时传入的参数 return function (){ let args = []; for(let i = 1;i<bounceArgs.length;i++){ args.push(bounceArgs[i]);} //从1开始遍历,因为bounceArgs[0] = mybind()参数列表中的obj for(let i =0;i<arguments.length;i++){args.push(arguments[i]);} //保存新函数的参数列表 return fn.apply(obj,args); }; } function add(y,z) { return this.x +y +z ; } let o = { x:1 } let func = add.mybind(o,2); //将函数绑定到o对象上,此时新函数中的this.x=o.x=1,同时传入参数y=2,该参数保存在bounceArgs[]中 console.log(func(3)); //传入参数z=3,该参数保存在新函数的arguments[]中; 输出结果"6"
若要考虑新函数的原型链问题,也即新函数的原型继承自原函数。
Function.prototype.mybind = function (obj){ let fn = this; let bounceArgs = arguments; let bounce = function (){ let args = [] for(let i = 1;i<bounceArgs.length;i++){ args.push(bounceArgs[i]);} //从1开始遍历,因为bounceArgs[0] = mybind()参数列表中的obj for(let i =0;i<arguments.length;i++){args.push(arguments[i]);} //保存新函数的参数列表 return fn.apply(obj,args); } bounce.prototype = Object.create(fn.prototype) //利用寄生式继承方式 return bounce; } function add(y,z) { return this.x +y +z ; } add.prototype.say = function (){ //原函数的原型属性有一个say方法 console.log('加法'); } let o = { x:1 } let func = add.mybind(o,2); console.log(func(3)); //仍然输出 6 func.prototype.say() //新函数也能调用原型中的say方法,输出“加法”