1、 接着看函数这个具有魔幻色彩的对象。在上篇文章中说函数内部属性时,还遗留了一个 this 内部属性没有解释,不过在说 this 之前,我想先说一说执行环境和作用域的概念。6、执行环境和作用域(1)执行环境(execution context):所有的 JavaScript 代码都运行在一个执行环境中,当控制权转移至 JavaScript 的可执行代码时,就进入了一个执行环境。活动的执行环境从逻辑上形成了一个栈,全局执行环境永远是这个栈的栈底元素,栈顶元素就是当前正在运行的执行环境。每一个函数都有自己的执行环境,当执行流进入一个函数时,会将这个函数的执行环境压入栈顶,函数执行完之后再将这个执行环
2、境弹出,控制权返回给之前的执行环境。(2)变量对象(variable object):每一个执行环境都有一个与之对应的变量对象,执行环境中定义的所有变量和函数就是保存在这个变量对象中。这个变量对象是后台实现中的一个对象,我们无法在代码中访问,但是这有助于我们理解执行环境和作用域相关概念。(3)作用域链(scope chain):当代码在一个执行环境中运行时,会创建由变量对象组成的一个作用域链。http:/ 这个链的前端,就是当前代码所在环境的变量对象,链的最末端,就是全局环境的变量对象。在一个执行环境中解析标识符时,会在当前执行环境相应的变量对象中搜索,找到就返回,没有找到就沿着作用域链一级一
3、级往上搜索直至全局环境的变量对象,如果一直未找到,就抛出引用异常。(4)活动对象(activation object):如果一个执行环境是函数执行环境,也将变量对象称为活动对象。活动对象在最开始只包含一个变量,即 arguments 对象(这个对象在全局环境的变量对象中不存在)。 这四个概念虽然有些抽象,但还是比较自然的,可以结合JavaScript 高级程序设计(第3 版) 中的一个例子来细细体会一下:/ 进入到全局作用域,创建全局变量对象var color = “blue“;function changeColor()/ 进入到 changeColor 作用域,创建 changeColor
4、 相应变量对象var anotherColor = “red“;function swapColors(color1, color2)/ 进入到 swapColors 作用域,创建 swapColors 相应变量对象var tempColor = anotherColor;anotherColor = color;color = tempColor;/* * swapColors 作用域内可以访问的对象有:* 全局变量对象的 color,changeColor* changeColor 函数相应变量对象的 anotherColor、swapColors* swapColors 函数相应变量对象
5、的 tempColor*/ swapColors(white);/* * changeColor 作用域内可以访问的对象有:* 全局变量对象的 color,changeColor* changeColor 函数相应变量对象的 anotherColor、swapColors*/http:/ changeColor();/* * 全局作用域内可以访问的对象有:* 全局变量对象的 color,changeColor*/这里的整个过程是:(1)进入全局环境,创建全局变量对象,将全局环境压入栈顶(这里也是栈底)。根据前面的关于声明提升的结论,这里创建全局变量对象可能的一个过程是,先创建全局变量对象,然后
6、处理函数声明设置属性 changeColor 为相应函数,再处理变量声明设置属性 color 为undefined。(2)执行全局环境中的代码。先执行 color 变量初始化,赋值为 blue,再调用changeColor()函数。(3)调用 changeColor()函数,进入到 changeColor 函数执行环境,创建这个环境相应的变量对象(也就是活动对象),将这个环境压入栈顶。创建活动对象可能的一个过程是,先创建活动对象,处理内部函数声明设置属性 swapColors 为相应函数,处理函数参数创建活动对象的属性 arguments 对象,处理内部变量声明设置属性 anotherColo
7、r 为 undefined。(4)执行 changeColor()函数代码。先执行 anotherColor 初始化为red,再调用swapColors()函数。(5)调用 swapColors()函数,进入到 swapColors 函数执行环境,创建相应的变量对象(活动对象),将 swapColors 执行环境压入栈顶。这里创建活动对象可能的一个过程是,先创建活动对象,处理函数参数,将形式参数作为活动对象的属性并赋值为 undefined,创建活动对象的属性 arguments 对象,并根据实际参数初始化形式参数和 arguments 对应的值和属性(将属性 color1 和 argumen
8、ts0初始化为white,由于没有第二个实际参数,所以color2 的值为 undefined,而 arguments 的长度只为 1 了),处理完函数参数之后,再处理函数内部变量声明,将 tempColor 作为活动对象的属性并赋值为 undefined。(6)执行 swapColors()函数代码。先给 tempColor 初始化赋值,然后实现值交换功能(这里 color 和 anotherColor 的值都是沿着作用域链才读取到的)。(7)swapColors()函数代码执行完之后,返回 undefined,将相应的执行环境弹出栈并销毁(注意,这里会销毁执行环境,但是执行环境相应的活动对
9、象并不一定会被销毁),当前执行环境恢复成 changeColor()函数的执行环境。随着 swapColor()函数执行完并返回,changeColor()也就执行完了,同样返回 undefined,并将 changeColor()函数的执行环境弹出栈并销毁,当前执行环境恢复成全局环境。整个处理过程结束,全局环境直至页面退出再销毁。作用域链也解释了为什么函数可以在内部递归调用自身:函数名是函数定义所在执行环境相应变量对象的一个属性,然后在函数内部执行环境中,就可以沿着作用域链向外上溯一层访问函数名执行的函数对象了。如果在函数内部将函数名指向了一个新函数,递归调用时就会不正确了:function
10、 fn(num)if(1 = num)return 1;elsefn = function()return 0;return num * fn(num - 1);console.info(fn(5);/0关于作用域和声明提升,再看一个例子:1 var name = linjisong;2 function fn()3 console.info(name);/undefined4 var name = oulinhai;5 console.info(name);/oulinhai6 7 fn();8 console.info(name);/linjisong这里最不直观的可能是第 3 行输出 u
11、ndefined,因为在全局中已经定义过 name 了,不过按照上面解析的步骤去解析一次,就可以得出正确的结果了。另外强调一下,在ECMAScript 中只有全局执行环境和函数执行环境,相应的也只有全局作用域和函数作用域,没有块作用域虽然有块语句。function fn()var fnScope = a;var blockScope = b; blockScope += fnScope; console.info(blockScope);/没有块作用域,所以可以在整个函数作用域内访问blockScopeconsole.info(fnScope);fn();/ba,aconsole.info(b
12、lockScope);/ReferenceError,函数作用域外,不能访问内部定义的变量console.info(fnScope);/ReferenceError插了一段较为抽象的概念,希望不至于影响整个阅读的流畅,事实上,我在这里还悄悄的绕过了一个称为“闭包” 的概念,关于函数与闭包,在下篇文章中再详细叙述。7、函数内部对象与 this对于面向对象语言的使用者来说,this 实在是再熟悉不过了,不就是指向构造函数新创建的对象吗!不过,在 ECMAScript 中,且别掉以轻心,事情没有那么简单,虽然在使用 new操作符调用函数的情况下,this 也的确是指向新创建的对象,但这只是指定 th
13、is 对象值的一种方式而已,还有更多的方式可以指定 this 对象的值,换句话说, this 是动态的,是可以由我们自己自由指定的。(1)全局环境中的 this在全局环境中,this 指向全局对象本身,在浏览器中也就是 window,这里也可以把全局环境中的 this 理解为全局执行环境相应的变量对象,在全局环境中定义的变量和函数都是这个变量对象的属性:var vo = a;vo2 = b;function fn()return fn;console.info(this = window);/trueconsole.info(this.vo);/aconsole.info(this.vo2);
14、/bconsole.info(this.fn();/fn如果在自定义函数中要引用全局对象,虽然可以直接使用 window,但更好的方式则是将全局对象作为参数传入函数,这是在 JS 库中非常通用的一种方式:(function(global)console.info(global = window);/在内部可以使用 global 代替 window 了)(this);这种方式兼容性更好(ECMAScript 的实现中全局对象未必都是 window),在压缩时,也可以将 global 简化为 g,而不用使用 window 了。(2)函数内部属性 this在函数环境中,this 是一个内部属性对象,
15、可以理解成函数对应的活动对象的一个属性,而这个内部属性的值是动态的。那 this 值是怎么动态确定的呢? 使用 new 调用时,函数也称为构造函数,这个时候函数内部的 this 被指定为新创建的对象。 function fn()var name = oulinhai;/函数对应的活动对象的属性this.name = linjisong;/当使用 new 调用函数时,将 this 指定为新创建对象,也就是给新创建对象添加属性var person = new fn();console.info(person.name);/linjisong需要注意区分一下函数执行环境中定义的属性(也即活动对象的属
16、性)和 this 对象的属性。 作为一般函数调用时,this 指向全局对象。 作为对象的方法调用时,this 指向调用这个方法的对象。 看下面的例子:var name = oulinhai;var person = name:linjisong,getName:function()return this.name; ;console.info(person.getName();/linjisongvar getName = person.getName;console.info(getName();/oulinhai这里函数对象本身是匿名的,是作为 person 对象的一个属性,当作为对象属性
17、调用时,this 指向了对象,当把这个函数赋给另一个函数然后调用时,是作为一般函数调用的,this 指向了全局对象。这个例子充分说明了“函数作为对象的方法调用时内部属性 this 指向这个调用对象,函数作为一般函数调用时内部属性 this 指向全局对象 ”,也说明了 this 的指定是动态的,是在调用时指定的,而不管函数是单独定义的还是作为对象方法定义的。也正是因为函数作为对象的方法调用时 this 指向这个调用对象,所以在函数内部返回 this 时才能够延续调用对象的下一个方法也就是链式操作(jQuery 的一大特色)。 使用 apply()、call()或 bind()调用函数时,this
18、 指向第一个参数对象。如果没有传入参数或传入的是 null 和 undefined,this 指向全局对象(在 ES5 的严格模式下会设为 null)。如果传入的第一个参数是一个简单类型,会将 this 设置为相应的简单类型包装对象。 var name = linjisong;function fn()return this.name;var person = name:oulinhai,getName:fn ;var person2 = name:hujinxing;var person3 = name:huanglanxue;console.info(fn();/linjisong,一般函
19、数调用,内部属性 this 指向全局对象,因此this.name 返回 linjisongconsole.info(person.getName();/oulinhai,作为对象方法调用,this 指向这个对象,因此这里返回 person.nameconsole.info(fn.apply(person2);/hujinxing,使用 apply、call 或 bind 调用函数,执行传入的第一个参数对象,因此返回 person2.nameconsole.info(fn.call(person2);/hujinxingvar newFn = fn.bind(person3);/ES5 中新增方
20、法,会创建一个新函数实例返回,内部this 值被指定为传入的参数对象console.info(newFn();/huanglanxue上面示例中列出的都是一些常见情况,没有列出第一个参数为 null 或 undefined 的情况,有兴趣的朋友可以自行测试。关于 this 值的确定,在原书中还有一个例子:var name = The Window;var object = name : My Object,getName:function()return this.name; ;console.info(object.getName();/My Objectconsole.info(objec
21、t.getName)();/My Objectconsole.info(object.getName = object.getName)();/The Window第 1 个是正常输出,第 2 个(object.getName) 与 object.getName 的效果是相同的,而第 3个(object.getName=object.getName)最终返回的是函数对象本身,也就是说第 3 个会作为一般函数来调用。8、函数属性和方法函数是一个对象,因此也可以有自己的属性和方法。不过函数属性和方法与函数内部属性很容易混淆,既然容易混淆,就把它们放一起对照着看,就好比一对双胞胎,不对照着看,不熟悉
22、的人是区分不了的。先从概念上来区分一下:(1)函数内部属性:可以理解为函数相应的活动对象的属性,是只能从函数体内部访问的属性,函数每一次被调用,都会被重新指定,具有动态性。(2)函数属性和方法:这是函数作为对象所具有的特性,只要函数一定义,函数对象就被创建,相应的属性和方法就可以访问,并且除非你在代码中明确赋为另一个值,否则它们的值不会改变,因而具有静态性。有一个例外属性 caller,表示调用当前函数的函数,也是在函数被调用时动态指定,在JavaScript 高级程序设计(第 3 版)中也因此将 caller 属性和函数内部属性 arguments、this 一起讲解,事实上,在 ES5 的
23、严格模式下,不能对具有动态特性的函数属性 caller 赋值。光从概念上区分是非常抽象的,也不是那么容易理解,再把这些属性列在一起比较一下:类别 名称继承性说明 备注this - 函数据以执行的环境对象 和一般面向对象语言有很大区别函数内部属性arguments -表示函数实际参数的类数组对象arguments 本身也有自己的属性:length、callee 和caller1、length 属性表示实际接收到的参数个数2、callee 属性指向函数对象本身,即有:fn.arguments.callee = fn3、caller 属性主要和函数的 caller 相区分,值永远都是 undefin
24、ed函 caller 否 调用当前函数的函数 虽然函数一定义就可访问,但是不在函数体内访问时永远为 null,在函数体内访问时返回调用当前函数的函数,在全局作用域中调用函数也会返回 nulllength 否 函数形式参数的长度 就是定义函数时命名的参数个数prototype 否 函数原型对象 原型对象是 ECMAScript 实现继承的基础数属性constructor 是继承自 Object,表示创建函数实例的函数,也就是Function()值永远是 Function,也就是内置的函数Function()apply 否调用函数自身,以(类)数组方式接受参数call 否 调用函数自身,以列举方式
25、接受参数bind 否 绑定函数作用域,ES5 中新增这三个方法主要作用是动态绑定函数内部属性this1、apply 和 call 在绑定之后会马上执行2、bind 在绑定之后可以在需要的时候再调用执行toLocalString 覆盖toString 覆盖valueOf 覆盖覆盖了 Object 类型中的方法,返回函数体不同浏览器实现返回可能不同,可能返回原始代码,也可能返回去掉注释后的代码hasOwnProperty 是propertyIsEnumerable 是函数方法isPropertyOf 是直接继承自 Object 类型的方法,用法同 Object函数属性和方法,除了从 Object
26、继承而来的属性和方法,也包括函数本身特有的属性和方法,用的最多的方法自然就是上一小节说的 apply()、call(),这两个方法都是用来设置函数内部属性 this 从而扩展函数作用域的,只不过 apply()扩展函数作用域时是以(类)数组方式接受函数的参数,而 call()扩展函数作用域时需要将函数参数一一列举出来传递,看下面的例子:function sum()var total = 0,l = arguments.length ;for(; l; l-)total += argumentsl-1; return total;console.info(sum.apply(null,1,2,3
27、,4);/10console.info(sum.call(null,1,2,3,4);/10不过需要强调的是:apply 和 call 的主要作用还是在于扩展函数作用域。apply 和 call 在扩展作用域时会马上调用函数,这使得应用中有了很大限制,因此在 ES5 中新增加了一个 bind()函数,这个函数也用于扩展作用域,但是可以不用马上执行函数,它返回一个函数实例,将传入给它的第一个参数作为原函数的作用域。它的一个可能的实现如下:function bind(scope)var that = this;return function()that.apply(scope, arguments); Function.prototype.bind = bind;这里涉及了一个闭包的概念,明天再继续。