JS核心知识点梳理——闭包、this

205 阅读8分钟

闭包

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 wiki

闭包这个东西咋说呢,不同的程序员,不同的资料都有不同的理解。你把它理解为一个函数,也可以把它理解为函数+执行环境。

我们这里不纠结闭包的定义。而是关注闭包的现象,应用,再结合相关面试题去攻克它,最后谈一下我对闭包的思考。

现象

之前我们说过了,函数执行,生成执行环境。函数执行完,销毁执行环境。嗯,听上去很正常,不用的东西就销毁嘛。

但是如果函数执行完,该函数上下文还用用怎么办,有用的东西肯定不敢销毁了。这个就是闭包的现象,那么能引起这个现象的鄙人就把它理解为闭包!

function foo () {
    var a = 1
    return function bar () {
        a++
        console.log(a)
    }
}
var b = foo() //函数执行完,我就问你foo的上下文你敢销毁吗?
b()  //  2
b()  //  3

大家看foo执行完的结果赋值给了b,也就是函数bar赋值给了b,未来我可能让b执行的,也就是让bar执行,所以我需要保证bar能访问上下文不被销毁。bar能访问的上下文实际上是foo执行时候的上下文。所以foo执行完以后上下文不会被销毁,会继续存在。

但是如果返回的是一个基本类型,则只需存在栈中就行,此时环境是会销毁的

所以,我的理解是 函数向外层作用域暴露出了一个内部引用,那么就存在闭包现象(真的没必要纠结闭包到底是啥,是函数?内部整体?是xxx?`。

作用与应用

作用1:保存变量

应用: 函数柯里化

var add = (a)=>(b)=> a+b
add(2)(3) //5
 

柯里化的分离参数有两个作用,一个是便于复用、管理,比如axios封装还有一个是便于理解比如react-redux的connect方法

作用2:保护变量(不受污染)

应用: 单例模式

 //jQ源码
 (function(){
      var jQuery=function(){
          //jq代码
       }
      window.$=window.jQuery=jQuery;  //这个通过window对象暴露给外面,根return的暴露是一个意思
   })()
//函数执行完了,里面的引用暴露给外面了,所以上下文不销毁

优点:变量和函数都写到立即执行函数里面了,避免了污染外层作用域

这个立即执行函数和闭包没啥关系,这个只是为了形成"块级作用域"

优缺点

闭包无处不在,也许你不经意就写了个闭包。

需要注意的是,由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

this

this是函数执行的时候OA中的一个变量。不同的执行环境指向不同

为什么引入this

因为我们解耦,为什么要解耦?因为我们要复用! 举个例子:我费了九牛二虎之力写了一个方法,可以对某个数组a进行复杂的操作

var a = [xxx,xxx,....]
function foo (){
  a xxx // do something to a
  xxx a  // do something to a
}

这个方法只能a用 耦合性太强。当其他数组b想用这个方法的时候由于方法里面的操作对象固定是a导致失败。 当然我也不能写成b,因为如果数组c要用难道我们再改成c? 怎么办,显然这个操作的对象不能是固定的,应该说最好是一个变量,谁调用,这个变量就是谁。 this就这么产生了!所以说this的可变是业务的需要,我们要的就是它的可变。当然你要是掌握不了它的变化规则,那么对你来说引入this就是一场灾难了。

this的规则

总原则: 函数中的this,指的是当前函数的执行主体;谁让函数执行的,那么this就指向谁

  • 在全局作用域下,this指向window;
  • 回调函数中的this指向window;
  • 自执行函数中的this永远指向window;
  • 匿名函数中的this指向window (常见闭包)
  • 函数体中的this,看函数执行前有没有".",如果有,那么点前面是谁,this就指向谁;如果没有“.”,那么会指向window;
  • 如果给元素的事件行为绑定方法,那么方法中的this,就会指向当前被绑定的那个元素;
  • call 、apply、bind可以改变this关键字;
  • 构造函数中的this指向当前类的实例;
  • 箭头函数里的this是外层OA的this

改变this

apply call bind可以改变this 箭头函数可以固化this 问题来了,为什么要改变this?

原因1 人家的方法,我想用

举个例子,Array类有reverse方法。只要是数组,都继承了Array的reverse方法,可以直接调用。

[1,2,3].reverse()  //[3,2,1]

现在有个类数组arguments,由于不属于Array类,没有办法继承reverse方法,但是我就是想用,咋办?改变this

Array.prototype.reverse.call(argumengts)

原因2 this太灵活,不符合预期

一般一个对象上的方法里的this指向这个对象。但是如果这个对象里的函数有闭包的话。 此时不管用匿名函数还是具名函数。里面的this都是windows。由于当前环境能找到this,所以不会去外层环境找。 此时我们需要用个不同的变量去拿个外层的this

var name = "The Window";

var object = { 
  name : "My Object",

  getNameFunc : function(){ 
  	return function(){ 
    	return this.name; 
    }; 
  }

};
//匿名函数调用
alert(object.getNameFunc()());
// 或者具名函数调用  都是拿到的windows的
let f = object.getNameFunc()
alert(f())

需要处理this 让函数体里面的this指向object

  getNameFunc : function(){ 
  	var that = this
  	return function(){ 
    	return that.name; 
    }; 
  }
  // 或者箭头函数 直接取外层环境的this
   getNameFunc : function(){ 
  	return ()=>{ 
    	return this.name; 
    }; 
  }

当然 也可以直接用call、bind

var name = "The Window";

var object = { 
  name : "My Object",

  getNameFunc : function(){ 
  	return function(){ 
    	return this.name; 
    }; 
  }

};
// 或者具名函数调用  都是拿到的windows的
let f = object.getNameFunc()
alert(f.call(object))

手写call

无非就是个改变this
需要注意1、添加属性不能重名还得删除2、参数的处理

Function.prototype.mycall = function(context=window||global,...args){
    let name = Symbol()
    context[name] = this
    let res =  context[name](...args)
    delete context[name]
    return res
}

面试题

1、下列代码执行结果

var name = 1; 
var object = { 
    name : 10,  
    getNameFunc: function(){
    var name = 100    
    this.name++
    return ()=>{ 
        name++
        this.name++
        console.log(this.name,name,window.name)  
    }; 
  }

};

let f = object.getNameFunc()
f()    // 12 101 1

2、下列代码执行结果

var n=0;
function a(){
    var n=10;
    function b(){
        n++;
        console.log(n);
    }
    b();
    return b;
}
var c=a();
c();
console.log(n);
//11 12 0

3、下列代码执行结果

var a=9; 
function fn(){ 
    a=0;       //如果这里是var a = 0 ,答案是多少
    return function(b){ 
        return b+a++; 
    }    
}
var f=fn();
console.log(f(5));
console.log(fn()(5));
console.log(f(5));
console.log(a);

// 5 5 6 2
// 如果是var a = 5 5 6 9

4、下列代码执行结果

var ary=[1,2,3,4];
function fn(ary){
    ary[0]=0;    
    ary=[0];    
    ary[0]=100;    
    return ary; 
}
var res=fn(ary);    
console.log(ary);    
console.log(res);
// [0,2,3,4] [100]

5、下列代码执行结果


function fn(i) {
    return function (n) {
        console.log(n + (i++));
    }
}
var f = fn(10);
f(20);
fn(20)(40);
fn(30)(50);
f(30);

//30 60 80 41

6、下列代码执行结果

var i = 10;
function fn() {
    return function (n) {
        console.log(n + (++i));
    }
}
var f = fn();
f(20);
fn()(20);
fn()(30);
f(30);
//31 32 43 44

7、以下代码的功能是要实现为5个input按钮循环绑定click点击事件,绑定完成后点击1、2、3、4、5五个按钮分别会alert输出0、1、2、3、4五个字符。(腾讯)

请问如下代码是否能实现? 如果不能实现那么现在的效果是什么样的? 应该做怎样的修改才能达到我们想要的效果,并说明原理?


<div id="btnBox">
    <input type="button" value="button_1" />
    <input type="button" value="button_2" />
    <input type="button" value="button_3" />
    <input type="button" value="button_4" />
    <input type="button" value="button_5" />
</div>
<script type="text/javascript">
    var btnBox=document.getElementById('btnBox'),
    inputs=btnBox.getElementsByTagName('input');
    var l=inputs.length;
    for(var i=0;i<l;i++){
        inputs[i].onclick=function(){
            alert(i);
        }
    }
</script>

1.不能实现 2.因为js没有块作用域,所以公用的外层作用域的i,当点击触发函数的时候 ,应当注意外层的i是5了,所以全部打印5没毛病 3. 解决思路1:没有块作用域我就用es6的let形成块作用域

 for(let i=0;i<l;i++){
	 inputs[i].onclick=function(){
	 	    alert(i);
	 }
}

解决思路2:每次绑定的时候i其实都是正确的,我能不能用另外一个变量将每次的i存起来呢?

for(var i=0;i<l;i++){
		inputs[i].onclick=(function(x){//x是形参,由于闭包存在,上下文不销毁
			return function () { 
				console.log(x)
			}
		})(i)
	}

8、下列代码执行结果


var num = 10;
var obj = {num: 20};
obj.fn = (function (num) {
    this.num = num * 3;
    num++;
    return function (n) {
        this.num += n;
        num++;
        console.log(num);
    }
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);
//22 23 65 30