首页 > 代码库 > 你一直想知道的关于JavaScript scope的一切

你一直想知道的关于JavaScript scope的一切

对于一个JavaScript初学者(甚至是有经验的JavaScript开发者)而言,JavaScript语言中关于“域”(scope)的一些概念并不是那么直白或是容易理解的。

由此,这篇文章旨在帮助那些在听说过诸如域(scope),闭包(closure),关键字this,命名空间(namespace),函数域(function scope),全局域(global scope),词法作用域(lexical scope)以及公共域和私有域(public/private scope)等词汇后,想要进一步学习JavaScript的朋友。

希望这篇文章可以帮助你找到下列问题的答案:

  • 什么是域?

  • 什么是全局域、本地域?

  • 什么是命名空间以及其与域之间的不同?

  • 什么是关键字以及它是如何受域影响的?

  • 什么是功能域、词法作用域?

  • 什么是闭包?

  • 什么是公共域、私有域?

  • 如何将上述概念融会贯通?

什么是域?

在JavaScript里,域指的是代码当前的上下文语境。域可以是公共定义的,也可以是本地定义的。理解JavaScript中的域,是你写出无可挑剔的代码以及成为更好的程序员的关键。

什么是全局域?

在你开始写一行JavaScript代码的时候,你正处在我们所说的全局域中。此时我们定义一个变量,那它就被定义在全局域中:

// global scope
var name = ‘Todd‘;

全局域是你最好的朋友,同时也是你最心悸的梦魇。学会控制各种域并不难,当你这么做之后,你就不会再遇到有关全局域的问题(多发生在与命名空间冲突时)。你或许经常听到有人说“全局域太糟糕了”,但却未听他们评判过个中缘由。其实,全局域并没有那么糟糕,因为你要在全局域当中创造可以被其他域所访问的模块和APIs,所以你必须学会扬长避短地使用它。

似乎大家都喜欢如此写jQuery代码,是不是你也这么干呢:

jQuery(‘.myClass‘);

。。。这样我们正在公共域中访问jQuery,我们可以把这种访问称之为命名空间。命名名空间在某些条件下可以理解为域,但通常它指的是最上层的域。在上面的例子里,jQuery作为命名空间存在公共域中。jQuery 命名空间在全局域中被定义,全局域就是jQuery库的命名空间,因为所有在命名空间中的东西都成为这个命名空间的派生。

什么是本地域?

本地域是指那些在全局域中定义的域。一般只能有一个全局域,定义其中的每一个函数都有自己的本地域。任何定义在其它函数里的函数都有一个连接那个外部函数的本地域。

假设我定义了一个函数,并在其中创建了几个变量,那这些变量就属于本地域。看下面的例子:

// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
};

任何属于本地域的物件对全局域都是不可见的-除非他们被暴露出来,也就是说,如果我在一个新的域中定义了一些函数和变量,它们是无法从当前那个域的外部被访问的。来看一个简单的例子:

var myFunction = function () {
  var name = ‘Todd‘;
  console.log(name); // Todd
};
// Uncaught ReferenceError: name is not defined
console.log(name);

变量name是属于本地域的,它没有暴露给它的父域,因此它是未定义的。

 函数域

在JavaScript中所有的域都是并且只能是被函数域(function scope)所创建,它们不能被for/while循环或者if/switch表达式创建。New function = new scope - 仅此而已。一个简单的例子来说明域的创建:

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};

创建新的域以及创建本地变量、函数、对象都是如此简单。

词法定义域

每当你看到一个函数在另一个函数里的时候,内部的那个函数可以访问外部的函数,这被称作词法定义域或是闭包 - 有时也被称作静态域。又来了,看下面这个例子:

// Scope A
var myFunction = function () {
  // Scope B
  var name = ‘Todd‘; // defined in Scope B
  var myOtherFunction = function () {
    // Scope C: `name` is accessible here!
  };
};

你会注意到 myOtherFunction 只是被简单的定义一下并没有被调用。调用顺序也会对域中变量该如何反应起到作用,这里我已经定义了一个函数然后在另一个Console下面调用了它:

var myFunction = function () {
  var name = ‘Todd‘;
  var myOtherFunction = function () {
    console.log(‘My name is ‘ + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// Will then log out:
// `Todd`
// `My name is Todd`

词法作用域很好用,任何定义在父域中的变量、对象、函数,都可以被子域链访问到,举个例子:

var name = ‘Todd‘;
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};

唯一需要记住的是词法作用域不能反过来用。这里我们看看词法作用域是如何不工作的:

// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = ‘Todd‘; // locally scoped
    };
  };
};

我总是可以返回一个引用给最上层的name,但却从来不是变量(‘Todd‘)本身。

 域链

域链给一个已知的函数建立了作用域。正如我们所知的那样,每一个被定义的函数都有自己的嵌套作用域,同时,任何被定义在其他函数中的函数都有一个本地域连接着外部的函数 - 这种连接被称作链。这就是在代码中定义作用域的地方。当我们在处理一个变量的时候,JavaScript就会开始从最里层的域向外查找直到找到要找的那个变量、对象或函数。

 闭包

闭包和词法作用域非常相近。一个关于闭包如何工作的更好或者更实际的例子就是返回一个函数的引用。我们可以返回域中的东西,使得它们可以被其父域所用。

var sayHello = function (name) {
  var text = ‘Hello, ‘ + name;
  return function () {
    console.log(text);
  };
};

我们此处所用的闭包使得sayHello里的域无法被公共域访问到。单是调用这个函数不会发生什么,因为它只是返回了一个函数而已:

sayHello(‘Todd‘); // nothing happens, no errors, just silence...

这个函数返回了一个函数,就是说它需要分配然后才是调用:

var helloTodd = sayHello(‘Todd‘);
helloTodd(); // will call the closure and log ‘Hello, Todd‘

好吧,我撒谎了,你可以调用它,或许你已经看到了像这样的函数,但是这会调用你的闭包:

sayHello2(‘Bob‘)(); // calls the returned function without assignment

AngularJS就为其 $compile 方法用了上面的技术,当前作用域作为引用传递给闭包:

$compile(template)(scope);

我们可以猜测代码或许应该像下面这样:

var $compile = function (template) {
  // some magic stuff here
  // scope is out of scope, though...
  return function (scope) {
    // access to `template` and `scope` to do magic with too
  };
};

一个函数不是只有返回什么东西的时候才会称作闭包。简单地使词法作用域的外层可以访问其中的变量,这便创建了一个闭包。

 作用域和关键字‘this’

每一个作用域都会根据函数的调用方式来绑定不同的 this 的值。我们都用过 this 关键字,但不是我们所有人都理解以及区别 this 在调用当中的变化。默认情况下 this 值得是做外层的公共对象 - window( node.js 里是 exports)。大概其看一下以不同方式调用函数时 this 值的不同:

var myFunction = function () {
  console.log(this); // this = global, [object Window]
};
myFunction();

var myObject = {};
myObject.myMethod = function () {
  console.log(this); // this = Object { myObject }
};

var nav = document.querySelector(‘.nav‘); // <nav>
var toggleNav = function () {
  console.log(this); // this = <nav> element
};
nav.addEventListener(‘click‘, toggleNav, false);

这里还有个问题,就算在同一个函数中,作用域也是会变,this 的值也是会变:

var nav = document.querySelector(‘.nav‘); // <nav>
var toggleNav = function () {
  console.log(this); // <nav> element
  setTimeout(function () {
    console.log(this); // [object Window]
  }, 1000);
};
nav.addEventListener(‘click‘, toggleNav, false);

那这里究竟发生了什么?我们新创建了一个不会从事件控制器调用的作用域,所以它也如我们所预期的那样,默认是指向 window 对象的。 如果我们想要访问这个 this 值,有几件事我们可以让我们达到目的。可能以前你就知道了,我们可以用一个像 that 这样的变量来缓存对 this 的引用:

var nav = document.querySelector(‘.nav‘); // <nav>
var toggleNav = function () {
  var that = this;
  console.log(that); // <nav> element
  setTimeout(function () {
    console.log(that); // <nav> element
  }, 1000);
};
nav.addEventListener(‘click‘, toggleNav, false);

用 call,apply 和 bind 改变作用域

有时你会根据需要更改作用域。一个简单的证明如何在循环中更改作用域:

var links = document.querySelectorAll(‘nav li‘);
for (var i = 0; i < links.length; i++) {
  console.log(this); // [object Window]
}

在这里 this 值 不是指我们的元素,我们没有调用任何东西或者改变作用域。让我们来看一下如何改变作用域(看上去我们改变的是作用域,但是我们真正在做的却是更改函数被调用的上下文语境)。

.call() and .apply()

.call() 和 .apply() 这两个方法的确很美好,他们允许你传递一个函数给作用域,并绑定正确的 this 值。让我们看一下如何将 this 绑定给上面例子中的每个元素:

var links = document.querySelectorAll(‘nav li‘);
for (var i = 0; i < links.length; i++) {
  (function () {
    console.log(this);
  }).call(links[i]);
}

你可以看到我传递了当前的元素数组迭代( links[i] ),它盖面了函数的作用域以至于 this 值变成了每个元素。 我们可以用 this 绑定任何我们想要的。我们可以用 call 或者 apply 任一方法改变作用域,他们的区别是: .call(scope, arg1, arg2, arg3) 接收的是用逗号隔开的独立参数,而 .apply(scope, [arg1, arg2]) 接收的是一个参数数组。

记得用 call() or .apply() 而不是像下面这样调用你的函数非常重要:

myFunction(); // invoke myFunction

You‘ll let .call() handle it and chain the method:

myFunction.call(scope); // invoke myFunction using .call()

.bind()

不同于上述方法,使用 .bind() 不会调用一个函数, 它只是在函数运行前绑定了一个值。ECMASCript5 当中才引入这个方法实在是太晚太可惜了,因为它是如此的美妙。如你所知,我们不能出传递参数给函数,就像这样:

// works
nav.addEventListener(‘click‘, toggleNav, false);

// will invoke the function immediately
nav.addEventListener(‘click‘, toggleNav(arg1, arg2), false);

我们可以通过在其中创建一个新的函数来搞定它:

nav.addEventListener(‘click‘, function () {
  toggleNav(arg1, arg2);
}, false);

还是那个问题,这个改变了作用域的同时我们也创建了一个不需要的函数,这对性能是一种浪费如果我们在循环内部绑定事件监听器。 尽管这使得我们可以传递参数进去,似乎应该算是 .bind() 的用武之地,但是这个函数不会被执行:

nav.addEventListener(‘click‘, toggleNav.bind(scope, arg1, arg2), false);

这个函数不会执行,并且作用域可以根据需要更改,但是参数还是在等待被传入。

私有域和公共域

在许多编程语言中,你将听到关于公共域和私有域,在 JavaScript 里没有这样的东西。但是我们可以通过像闭包一样的东西来模拟公共域和私有域。

我们可以通过使用 JavaScript 设计模式比如模块模式,来创建公共域和私有域。一个简单的创建私有域的途径就是把我们的函数包装进一个函数中。如我们之前学到的,函数创建作用域来使其中的东西不可被全局域访问:

(function () {
  // private scope inside here
})();

我们可能会紧接着创建一个新的函数在我们的应用中使用:

(function () {
  var myFunction = function () {
    // do some stuff here
  };
})();

当我们准备调用函数的时候,它不应在全局域里:

(function () {
  var myFunction = function () {
    // do some stuff here
  };
})();

myFunction(); // Uncaught ReferenceError: myFunction is not defined

成功!我们就此创建了一个私有域。但是如果我像让这个函数变成公共的,要怎么做呢?有一个很好的模式(被称作模块模式)允许我们正确地处理函数作用域。这里我在全局命名空间里建立了一个包含我所有相关代码的模块:

// define module
var Module = (function () {
  return {
    myMethod: function () {
      console.log(‘myMethod has been called.‘);
    }
  };
})();

// call module + methods
Module.myMethod();

在这里,return 的东西就是 public 方法返回的东西,它可以被全局域访问。我们的模块来关心我们的命名空间,它可以包含我们想要任意多的方法在里面:

// define module
var Module = (function () {
  return {
    myMethod: function () {

    },
    someOtherMethod: function () {

    }
  };
})();

// call module + methods
Module.myMethod();
Module.someOtherMethod();

那私有方法呢?这里是很多开发者做错的地方,他们把所有的函数都堆砌在全局域里以至于污染了整个全局命名空间。可工作的函数代码不一定非在全局域里才行,除非像 APIs 这种要在全局域里可以被访问的函数。这里我们来写一个没有被返回出来的函数:

var Module = (function () {
  var privateMethod = function () {

  };
  return {
    publicMethod: function () {

    }
  };
})();

这就意味着 publicMethod 可以被调用,但是 privateMethod 则不行,因为它被域私有了!这些私有的函数可以是任何你能想到的对象或方法。

但是这里还有个有点拧巴的地儿,那就是任何在同一个域中的东西都可以访问同一域中的其他东西,就算在这儿函数被返回出去以后。也就是说,我们的公共函数可以访问私有函数,所以私有函数依然可以和全局域互动,但是不能被全局域访问。

var Module = (function () {
  var privateMethod = function () {

  };
  return {
    publicMethod: function () {
      // has access to `privateMethod`, we can call it:
      // privateMethod();
    }
  };
})();

这种互动是充满力量同时又保证了代码安全。JavaScript中很重要的一块就是保证代码的安全,这就解释了为什么我们不能接受把所有的函数都放在公共域中,因为这样的话,他们都被暴露出来很容易受到攻击。

下面有个例子,返回了一个对象,用到了 public 和 private 方法:

var Module = (function () {
  var myModule = {};
  var privateMethod = function () {

  };
  myModule.publicMethod = function () {

  };
  myModule.anotherPublicMethod = function () {

  };
  return myModule; // returns the Object with public methods
})();

// usage
Module.publicMethod();

比较精巧的命名方式就是在私有方法名字前加下划线,这可以帮我们在视觉上区分公共的和私有的方法:

var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
})();

这里我们可以借助面向对象的方式来添加对函数的引用:

var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
  return {
    publicMethod: publicMethod,
    anotherPublicMethod: anotherPublicMethod
  }
})();

英文原文链接:http://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/

中文原文链接:https://my.oschina.net/bgmemo/blog/195761/

你一直想知道的关于JavaScript scope的一切