理解Javascript中的变量作用域(scope)和语境(context)

来源:互联网 发布:用dos运行java 编辑:程序博客网 时间:2024/05/22 13:42

Javascript语言是一门十分灵活的语言,所以其变量语境和作用域(context and Scope)也具有该语言的独有特性。函数可以在各种语境和作用域(context and Scope)中被封装和保存。这些特性导致Javascript可以给用户提供最强大的设计模式。然而,这些特性同时会给开发人员造成很大的混乱。所以,本文给出该语言作用域的全面的解释,以及如何使用它们进行各种模式的设计。

1语境与作用域(context vs Scope)

首先要强调的是语境和作用域并不是一个概念,但是很多开发者都会把这两个概念弄混。每个函数调用都有一个语境与作用域与之关联。从根本上来说,作用域(scope)是基于函数的,而语境(context)是基于对象的。换个说法,作用域(scope)涉及到一个函数被调用时候的变量访问,且每次调用都是唯一的。而语境(context)通常是‘this’关键字的值,该值指向‘拥有’当前执行代码的对象。

2变量作用域(variable Scope)

一个变量可以被定义为局部变量或者是全局变量,在运行时候可以在不同范围被调用。任何被定义的全局变量,指的是被定义在函数之外的变量,这些变量可以被函数在任何范围内访问和改变,且在程序运行时一直存在。局部变量只是在定义它的函数体内存在,在函数每次被调用的时候,函数内的局部变量其实是不一样的。局部变量只在调用其的函数内部可以被分配、检索、操作,在函数作用域之外是不能被访问的。

Javascript并不支持块级作用域,其作用域都是以函数为单位的,而不是以循环语句等以大括号{}括起来的语句为单位。也就是说函数内的如循环等大括号内定义变量在函数内部都可以被访问到。但是这个特征很快会发生变化,let 关键字已经被官方加入到ES6规范中,该关键字可以替代var关键字,并支持定义块级局部变量。

3什么是“this”的语境

语境通常可以被一个函数如何被调用所定义,当一个函数被作为一个对象的方法,this被设置为对象的方法如下:

var obj = {    foo: function(){        alert(this === obj);        }};<span style="white-space:pre"></span>obj.foo(); // true
该规则在使用new关键字的时候也同样适用,当以该方法调用的时候,this的值会被设置为新创建的实例。

function foo(){    alert(this);}foo() // windownew foo() // foo
从上面的代码还可以看出,当作为一个未被绑定的函数调用时,this会被默认设置为浏览器的全局语境或者window对象。但是,在strict模式下,语境会被设置为undefined。

4执行语境

Javascript是个单线程的语言,意味着同一时间只有一个任务可以被执行,当Javascript解释器初始化执行代码时候,解释器会默认自动进入一个全局执行语境,从该点开始

,每个函数的调用将会创建一个新的执行语境。

这点往往会导致出现混乱,因为在这里“执行语境(execution context)”往往指的是作用域(scope)而非语境(context),这更像是一个命名的错误。

每次创建一个执行语境的时候,都会在执行堆栈顶端加一个执行语境。浏览器在运行时候会在该堆栈的最顶端也就是当前的执行语境中运行。一旦执行完毕,就会执行退栈

操作并改变当前的执行语境。

一个执行语境可以被分为创建阶段和执行阶段,在创建阶段,解释器会首先创造一个变量对象,或者被称为激活对象,它由该执行语境内的所有变量、函数声明、参数声明

组成。然后,作用域链(the scope chain) 也被初始化。最后this被确定下来。然后在执行阶段,代码会被解释与执行。

5作用域链(the scope chain)

在每个执行语境中,有一个作用域链与之对应。作用域链包括执行堆栈中的每个执行语境的变量对象。它被用来确定变量访问和标识符解析。例如:

function first(){    second();    function second(){        third();        function third(){            fourth();            function fourth(){                // do something            }        }    }   }first();
运行前面的代码将会导致嵌套函数被执行,最后到fourth函数。在这一点上,作用域链将会从顶到底分别为:fourth、third、second、first、global,最地段的函数可以访问下面

作用域的任何变量。

在不同的执行语境中的变量命名冲突解决方式是:在作用域链中从局部到全局移动。这就意味着,同名的局部变量在作用域链中具有较高的优先级。

简而言之,函数在执行中会首先访问自己的作用域中的变量,然后依次搜索作用域链,直到找到该变量。

6闭包

访问当前作用域之外的变量会创建一个闭包。换句话说,一个嵌套的函数在当前函数之外定义,就会形成一个闭包。闭包允许访问外部的函数变量。返回的嵌套函数允许用户维持对当前函数的外部函数的局部变量、参数、以及内部函数声明的访问。这个封装允许我们在外部语境隐藏与保存执行语境,并留下一个公共接口进行进一步的操作。一个简单的例子如下:

function foo(){    var localVariable = 'private variable';    return function bar(){        return localVariable;    }}var getLocalVariable = foo();getLocalVariable() // private variable
最流行的一种闭包被称为模块模式;它允许用户模拟公共、私有、特权成员:

var Module = (function(){    var privateProperty = 'foo';    function privateMethod(args){        // do something    }    return {        publicProperty: '',        publicMethod: function(args){            // do something        },        privilegedMethod: function(args){            return privateMethod(args);        }    };})();
该模块执行就像一个单例,在编译器解读后就执行完毕。在外部唯一可用的成员是返回的公共方法与特征,如Module.publicMethod。然而,所有的私有属性与方法将会在程序运行期间都存在。意味着变量可以通过公共方法被进一步访问与操作。

另外一种闭包被称为立即调用函数表达式(IIFE),是一种自我调用的匿名函数在全局环境中被执行。

(function(window){              var foo, bar;    function private(){        // do something    }    window.Module = {        public: function(){            // do something         }    };})(this);
闭包可以保证不影响全局命名空间,同时在函数体中声明的局部变量会在程序运行时也会一直存在。在框架和应用中,这是一种很好的封装源代码的方法,特别是当只留下一个全局接口进行交互的时候。
7 call与apply

这两个方法所具有的功能允许你执行任何函数在任何期望的语境中。call函数要求将参数灵位显式的参数,而apply要求将参数作为一个数组传入:

function user(firstName, lastName, age){    // do something }user.call(window, 'John', 'Doe', 30);user.apply(window, ['John', 'Doe', 30]);
这两个方法的调用结果是相同的,user函数在全局语境中被调用,并给予了三个一样的参数。

ECMAScript 5 (ES5)介绍了 Function.prototype.bind方法用来操作语境。它返回一个函数,该函数被永久绑定到第一个参数上。对于不支持该方法的浏览器,该方法也很容易实现:

if(!('bind' in Function.prototype)){    Function.prototype.bind = function(){        var fn = this,         context = arguments[0],         args = Array.prototype.slice.call(arguments, 1);        return function(){            return fn.apply(context, args.concat([].slice.call(arguments)));        }    }}

该方法通常用在语境常会消失的地方,以及面向对象与事件处理中。该方法通常是必要的,因为一个节点的addEventListener方法通常也应当在事件处理程序绑定到节点的语境中执行回调函数。然而如果你使用面向对象技术,要求回调函数是实例的一个方法,你就会被要求手动的适应该语境,这就是bind函数方便的地方:

function MyClass(){    this.element = document.createElement('div');    this.element.addEventListener('click', this.onClick.bind(this), false);}MyClass.prototype.onClick = function(e){    // do something};
两个涉及数组调用slice方法:

Array.prototype.slice.call(arguments, 1);[].slice.call(arguments);
该技术调用另一个对象方法的时候适用于面向对象,类似于继承:

MyClass.prototype.init = function(){    // call the superclass init method in the context of the "MyClass" instance    MySuperClass.prototype.init.apply(this, arguments);}
7结论
作用域(scope)与语境(context)在现代Javascript中扮演一个基础的角色,所以理解其概念对于进行高级设计模式十分有必要。不论是否讨论闭包、面向对象、继承,这两个概念都扮演了一个重要的作用。如果你的目标是掌握Javascript语言,并更好的理解其他概念,这两个概念应该是一个起点。


英文原文:http://ryanmorr.com/understanding-scope-and-context-in-javascript/


注:本文将scope翻译为作用域,将context翻译为语境





0 0
原创粉丝点击