闭包,this指向,作用域,绑定对象

来源:互联网 发布:张卫健 西游记 知乎 编辑:程序博客网 时间:2024/05/16 17:04

说完了面向对象以及继承,今天再来说说this指向及作用域的问题,许多人都觉得js很简单,但是我学了这么多语言,并不觉得js简单呀T_T,其中难点之一就是this指向和作用域的问题了,先来说说作用域,我们一点一点来分析:

一、无块级作用域

function fn1(){for (var i = 0; i < 5; i++) {//do somthing}if(false){var j=5;}console.log(i);console.log(j);}fn1();

以上代码相信大家都写过,大家觉得结果是什么呢,是undefined和报错吗,no,先来上结果


结果是5和undefined,在C语言及其它语言中,花括号里面定义的变量是对外不可见的,但是在js中却不是,在js中是以function为单位的,那是因为在函数执行的时候会有一个活动对象,

作用域链:

为什么会有作用域链,那是因为js与其它语言不同,在其它语言中是不能在函数中定义函数的,但是在js中却可以,比如下面这样:

function fn1(){var i=5;function fn2(){var c=1;}}
在上面这种情况,fn2里面是可以访问fn1里面定义的i的,那是为什么呢,那是因为在执行阶段的时候,每个执行环境会为函数分配一个变量对象,比如fn2的变量对象上面存的是c,arguments,arguments是js动态添加的。而fn1的变量对象有i,fn2,arguments,那fn2怎么访问fn1里面的i的呢,其实这个变量对象只是一部分,在执行的时候,函数会创建一个作用域链对象,这个对象可以看作是一个类数组的形式,我就稍微画一下fn2的作用域链。

画得比较丑,嘿嘿,正如大家所见,fn有一个指针指向了作用域链这个类数组的东西,首先它会找到作用域链的0的位置,也就是本函数的变量对象,里面的属性会包含所有在function里面用var声明的变量,不管是在if里面,还是在for里面,只要不是在里面的function里面,都会变成变量对象的属性,这就是预编译过程,并且会为每个变量赋初值为undefined,当然还会有一些变量,比如形式参数(如果设置了形参的话,其实和argument[0],argument[1]。。。等是一个变量,也可以互相修改),如果没找到,那么继续往上找,找到作用域链的1的位置上,也就是fn1的变量对象,这里的arguments是访问不到的,因为在fn2的变量对象上已经找到了,然后在作用域链的1的位置上还是找不到的话,就继续往上找,找到全局对象上,也就是windows对象上,访问它的属性。我们来稍微分析一下arguments。

function fn1(){var i=5;var ar=arguments;function fn2(){var c=6;console.log(ar);}return fn2;}var fn=fn1(1);fn();

在fn1里面,我把fn1的arguments赋值给了ar,然后在fn2里面打印了出来,证明结果是有的,这个callee指向函数本身大家都知道,跟fn1.prototype.constructor一样,都是指向本身函数,当然这里不讨论这个。这就是传说中的闭包,把一个函数返回出去,然后这个函数的作用域链指针指向fn1也就是外层函数的变量对象,所以在这里,fn1里面的i是一直存在内存里的,因为在全局环境里,有对fn1的变量对象的引用,要消除引用,可以用

fn=null的形式,这样就断开了对内存函数的引用,自然就断开了fn1的变量对象的引用,js垃圾回收机制就会自动回收。要注意,这里的作用域链是在定义的时候就冥冥之中确定好了的,只是在执行的时候才分配内存而已,我们可以验证一下:

function fn1(){var i=5;function fn2(){var c=6;console.log(k);}return fn2;}(function(){var k=5;var fn=fn1();fn();})();

有些人会认为这里的结果是5,但是结果却是出错,因为在作用域链里面根本找不到这个k的定义。

到这里,大家应该对作用域链差不多有一定的了解了

再来看看this指向,this指向恰好跟虽然是对象中才出现的名词,但是也出现在函数中,因为函数本身也是一个对象,谁执行,那么this就指向谁,在函数里,默认是window对象执行,所以可以看作是window对象的方法,所以也就是window了。

验证验证:

function fn1(){console.log(this);}fn1();


大家看到了,结果是window,那我们换一种方式呢。

function fn1(){console.log(this);}var object=new Object();object.fn1=fn1;object.fn1();

大家看到了,居然是object,也就是this对象随着执行对象的改变,是可以改变的,我们再来看看什么叫函数表达式,什么叫方法。

function F(){this.data="5";this.say=function(){alert(this.data);}}var a=new F();var c=a.say;c();

大家看到了,我只是将对象的方法赋值给了c,然后执行,就找不到data属性了,那是因为这个赋值表达式其实默认已经将this指向给修改了,现在say方法里面的this已经指向window了,而这个c就是函数表达式,可以这样想,window的方法就叫函数表达式吧,而其它对象的方法就叫做方法,它们的this指向不一样。那我们继续跟作用域链扯上关系试试:

function F(){this.data="5";var data2="F";this.say=function(){console.log(data2);console.log(this.data);}}var a=new F();var c=a.say;c();


这个结果大家明白是为什么吗,虽然函数被绑定到了windows对象上,但是作为函数,say方法在冥冥之中已经注定了作用域链,不管绑定给谁,或者在哪执行,作用域链已经确定了,改变不了,所以可以打印出data2,写到这里,我们再来看看一些比较迷惑的写法:

function F(){this.data="5";this.say=function(){console.log(this.data);}}var a=new F();(a.say)();

这里结果是5,我就不传了,我们再来看看另外一种。

function F(){this.data="5";this.say=function(){console.log(this.data);}}var a=new F();(a.say=a.say)();

这里只有最后一行改变了,但是结果却是undefined,那是因为在前面的括号里面执行赋值语句话,返回的将是函数表达式,也就是绑定到了window对象上去了。再来看看令人迷惑的setTimeout

function F(){this.data="5";this.say=function(){console.log(this.data);}}var a=new F();setTimeout(a.say,10);


正如大家所见,在setTimeout里面也变成了函数表达式,this指向也变成了window,那我们再换一换:

function F(){this.data="5";this.say=function(){console.log(this.data);}}var a=new F();setTimeout(function(){a.say();},10);


为什么这里就能正确打印出来呢,那是因为在外层是个函数表达式没错,但是在这个表达式内部却没有将say方法变成函数表达式,而是让其执行,当然绑定的是a对象了,就像我之前写的第一篇博客一样,第一篇博客写的不是很好,因为那个时候没有理解到这里。我们再温习一下:

function F(){this.data="5";this.say=function(){console.log(this.data);}this.say2=function(){setTimeout(this.say,10);}}var a=new F();a.say2();

类似于这样,最后打印的结果是undefined,为什么是这样呢,首先在say2方法里面这个this还是绑定在a对象上的,因为在外层调用的时候没有使用函数表达式,也就是this.say是可以找到的,如果换成var c=a.say2;c();这样执行的话,连setTimeout里面的say方法都是找不到的,因为绑定到window上去了,这里是可以找到say方法的,但是say方法里面的this是那个对象呢,刚刚说了setTimeout里面会变成函数表达式,也就是window,那么this.data肯定是找不到的,那为什么会发生这样的情况呢,就是为什么会产生函数表达式这种说法呢,来看看下面这个例子:

function exec(fn){fn();}function F(){this.data="5";this.say=function(){console.log(this.data);}this.say2=function(){setTimeout(this.say,10);}}var a=new F();exec(a.say);

这里,我定义了一个exec方法,然后在exec方法里面执行这个函数,然后就变成了undefined,那是因为传参的时候,是传的函数表达式,也就是不知不觉就改变了this指向,说到这里,大家应该对this指向也比较了解了,以前我经常将作用域链和原型链搞混,经过自己分析之后,妈妈就再也不用担心我的学习了。结尾我们再来看看一个牛逼的方法,bind方法,我们来看看bind方法的定义。

function bind(obj,fn){<span style="white-space:pre"></span>return function(){<span style="white-space:pre"></span>fn.apply(obj,arguments);<span style="white-space:pre"></span>}}
这里将一个方法绑定到一个对象上,你也可以用赋值语句来实现

function bind(obj,fn){obj.fn=fn;}

这样也绑定上去了,但是这样不就多了一个属性么,这样不好,上面那个绑定却是返回一个函数,这个函数可以绑定给任何对象,我们来看看绑定的例子

var div1=document.getElementById("div1");function F(){this.data="5";this.say=function(){console.log(this.data);}}var a=new F();div1.onclick=a.say;




结果是undefined,那是因为this对象被绑定到div1这个dom对象上了,但是我还是想它弹出5,应该怎么办呢?我们可以借用seTimeout的思想

div1.onclick=function(){a.say();};

这样就行了,因为这样this指针就是绑定在a对象上面的,当然还能这样:

var div1=document.getElementById("div1");function F(){this.data="5";this.say=function(){console.log(this.data);}}var a=new F();div1.onclick=bind(a,a.say);function bind(obj,fn){return function(){fn.apply(obj,arguments);}}


结果还是5,那是因为返回了一个函数表达式,里面用了一个apply修改了fn的this指向,至于你想用第一种还是第二种,那就看你自己了,也分情况,如果这个方法是这个对象上存在的,就推荐第一种,如果是这个方法对象上不存在,那么就推荐第二种,我们来看看什么时候用第二种:

var div1=document.getElementById("div1");function F(){this.name=5;this.data="5";this.say=function(){console.log(this.data);}}function sayName(){alert(this.name);}var a=new F();div1.onclick=bind(a,sayName);function bind(obj,fn){return function(){fn.apply(obj,arguments);}}

这个sayName是函数或者其它对象上的,我想绑定在a对象上,那这个时候用bind就非常好了,如果是本对象存在的方法调用,那就用第一种吧,好了,今天就分析到这里了,谢谢大家!



0 0