参考资料javascript
代码的执行所处的环境,也叫执行上下文,它肯定了代码的做用域,做用域链,this属性,代码的生存期等等,让咱们从解释器的角度看代码是如何执行的。
EC能够用以下的数据结构表达,它有不少属性,VO,[[scope]],this等等。html
EC={ Variable Object:...., [[scope]]:..., this:... }
一段JS代码进入解释器到执行分为2步骤,这2步各自有各自的事要处理java
咱们声明的变量,声明的函数,传入的参数都放到哪里去了?引擎是在哪里寻找它们的?其实它们都放入到一个叫VO的对象里去了,能够说了解VO是很重要的。VO的数据结构能够以下表达chrome
VO={ 声明的变量, 声明的函数, 参数(函数内部VO拥有) }
ECMAScript是经过上下文来区分的,若是function foo(){}是做为赋值表达式的一部分的话,那它就是一个函数表达式,反之为函数声明式。数组
function foo(){} // 声明 var bar = function foo(){}; // 表达式,由于它是赋值表达式的一部分 new function bar(){}; // 表达式,由于它是new表达式 (function(){ function bar(){} // 声明 })(); (function foo(){}); // 函数表达式:包含在分组操做符()内,而分组符里的表达式
注意:根据表达式的生成函数的函数名称是不会放入函数所处的EC的VO中的。浏览器
function f() { var bar = function foo() {};//这是表达式声明函数 typeof bar;//function; typeof foo;//undefined; } f.VO.foo=undefined;//不管是在代码进入环境仍是代码执行的时候
可是这个foo只在foo函数EC中有效,由于规范规定了标示符foo不能在外围的EC有效,并且是在foo的VO中存在,有些浏览器(chrome)是没法用debug访问到的,可是firefox是能够访问到的,可是IE6~IE8是在foo的外围能够访问到foo的,IE9已经修复了这个问题,能够用IE8执行以下代码。数据结构
alert(typeof foo);//undefined var bar = function foo() { alert(typeof foo);//function function k() { } return function () { alert(typeof foo);//function alert(typeof k);//function } } bar()();
EC肯定了VO的不一样,因此按EC给VO分类。闭包
全部在global声明的函数,变量都会在global的VO中存在。app
global.vo = { Math: <...>, String: <...> ... ... window: global //引用自身 };
当进入执行上下文VO会有以下属性:dom
函数的全部形参(若是咱们是在函数执行上下文中)
由名称和对应值组成的一个变量对象的属性被建立;没有传递对应参数的话,那么由名称和undefined值组成的一种变量对象的属性也将被建立。
function f(a, b, a) { debugger; } f(1, 2, 3);
执行的时候f的VO
f.VO={ a:3, b:2 }
由于形参名字重复,而VO的key是不能够重复的(VO是一个对象),因此在代码执行给VO赋值时根据前后顺序最后一个实参会覆盖第一个实参的值。
全部函数声明(FunctionDeclaration, FD)
由名称和对应值(函数对象(function-object))组成一个变量对象的属性被建立;若是变量对象已经存在相同名称的属性,则彻底替换这个属性。
全部变量声明(var, VariableDeclaration)
由名称和对应值(undefined)组成一个变量对象的属性被建立;若是变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
能够看到声明的函数优先级大于变量的声明。
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
test1
function f(a, b, c) { var a = a, g = 1; function g() { //function body } var k = 1; } f(1, 2, 3);
引擎进入执行环境时,把EC中的变量,形参,声明的函数放入VO,成为VO的属性
f.VO={ a:undefined,//这个是形参的a,优先级高于声明的变量a b:undefined,//这个是形参的b c:undefined,//这个是形参的c g:function,//函数的优先级最高,覆盖了变量g k:undefined//声明的k };
代码执行时给VO属性赋值,按代码执行过程
f.VO={ a:1, b:2, c:3, g:function, k:1 };
test2
function f(a) { a = a; b = a; } f(1); alert(a);//undefined alert(b);//1
test2很奇怪是吧,咱们认为会alert(a)提示“1”,可是结果是undefined。在咱们的脑海里老是有这个概念:没有声明的变量会变成全局变量,其实根本没这回事。事实是:给没有声明的变量赋值形成的现象是变量变为了global的属性(也就是window属性),而不是一个全局变量。让咱们来看下代码的流程。
进入环境,把形参标示放入VO中,并赋值为undefined
f.VO={ a:undefined }
这里没有把b算入f的VO,由于b不是声明出来的
执行时
形参a根据实参1,被赋予1,代码第一行a=a,右边的a为1,解释给左边的a赋值,解释器从VO开始寻找a,发现VO中有a,就给其赋予形参a的值——1。解释器寻找b,从[[scope]]寻找一直到global的VO都没找到b,因而就给global添加一个属性——b,赋值于1。期间没有产生新的变量。
test3
function f() { var a = 1; return { set:function (b) { a = b; }, get:function () { return a; } } } var o=f(); o.set(2); o.get();//2
总结:
[[scope]]是函数的内部属性,它指向一个数组对象(俗称做用域链对象),这个数组对象会包含父亲函数的VO一直到global的VO。
[[scope]]-->VO+[[scope]]
这个对象在2种环境(进入执行环境,执行代码)有着不一样状态。
eg
function f() { var a = 1; }
针对这个函数来讲。
进入执行环境(执行代码前),函数的EC中的VO和[[scope]]
f.VO={ a:undefined } f.scope=[global.VO];//全局vo在进入f的执行环境前已经建立了。
执行时,把f的VO推入[[scope]]指向的数据对象第一位。
f.VO={ a:1 } f.scope=[f.VO,global.VO];
catch,with关键词会在执行时把参数推入[[scope]]指向的数组对象第一位
witch({a:1}){ alert(a);//1 var a=2; alert(a);//2 }
进入环境
vo={ a:undefined }
执行
scope=[{a:1},vo,global.vo]--->alert(a)//1 var a=2;//执行到这里时a的值发生了改变而且影响到scope scope=[{a:2},vo,global.vo]--->alert(a)//2,
从本质上了解了做用域链,就很容易理解闭包了。
当函数内部定义了其余函数,就建立了闭包,闭包(子函数)有权访问父级函数的VO全部变量。
若是子函数[[scope]]持续引用了父函数的VO,就会使父函数的VO没法销毁掉。因此咱们要妥善处理闭包的特性。
function f() { var val = 1; return function () { return val; } } var temp=f();
返回的函数[[scope]]持有f函数的VO,已至于f执行后没法释放VO等所占用的内存。
var temp=null;//让GC去处理f的内存吧。
this是在代码进入执行环境时确认的,因此按代码进入执行环境时所在说明this更为清晰。
代码在global中执行,this永远都是global。
函数的EC中的this是由函数调用的方式来肯定的。
this指向这些函数的第一个参数
var sth = "global"; function f() { alert(this.sth); } f();//global var o = {sth:"o"}; f.apply(o);//o f.call(o);//o
这是的函数叫构造函数
执行时this的值取决于()左边的值所属的对象
若是函数被引用,那么this指向这个引用函数的东东的所属环境,可是函数被函数的VO引用那么this指向null,再而转为global。
test1
var sth = "global"; function f() { alert(this.sth); } f();//global var o = {sth:"o"}; o.f = f; o.f();//o
test2
var a = 'global'; function f() { alert(this); } f();//global f.prototype.constructor();//f.prototype
test3
这种状况下,f被k的vo引用,f的执行环境的this指向null,转为global。
function k() { function f() { alert(this); } f();//window } k.vo.f=function; f.this=null==>global;
test4
var foo = { bar: function () { alert(this); } }; foo.bar(); // Reference, OK => foo ()左边的引用类型属于foo,this指向foo (foo.bar)(); // Reference, OK => foo “()”对foo.bar没有任何处理,返回还是foo.bar。 (foo.bar = foo.bar)(); // global 赋值操做符使返回值是foo.bar所指向的函数。返回的是一个没有东西引用的function (false || foo.bar)(); // global ||同上 (foo.bar, foo.bar)(); // global 连续运算符还是同上
this指向null,可是浏览器不会让你这么干,它会把null变为global。
注:第5版的ECMAScript中,已经不强迫转换成全局变量了,而是赋值为undefined。
(function (){ alert(this);//window })();
eval('alert(this)');//window
以这个概念注册事件处理函数有2种实现方法,可是异曲同工——都是给节点对象的事件属性注册事件处理函数。
<div onclick="alert(this.innerHTML);">1</div> ==>'1'
<div id="J_Demo1">2</div> <script type="text/javascript"> document.getElementById("J_Demo1").onclick = function () { alert(this.innerHTML);//2 }; </script>
其实说这2个方法对理解function的this有点跑偏,可是仍是要标记下。
2这不一样的是addEventListener绑定事件处理函数后函数的this指向这个节点对象,attachEvent指向window(attachEvent存在于<=IE8)。
test1
有时候咱们想这样作
var $=document.getElementById;//引用这个方法 $("J_Head");//Illegal invocation 非法调用
为何会这样呢?由于咱们调用getElementById的方式不对。$()执行是getElementById的中的this指向的是window,用document.getElementById方式调用getElementById,其this指向的是document,缘由已经说过了。因此...
$.apply(document,["login-container"]);//指定this指向对象就ok了 (1,document.getElementById)();//Illegal invocation,连续运算符将getElementById已经从document中取出,执行时this指向null,进而指向global