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

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

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

但愿这篇文章能够帮助你找到下列问题的答案:node

  • 什么是域?程序员

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

  • 什么是命名空间以及其与域之间的不一样?设计模式

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

  • 什么是功能域、词法做用域?缓存

  • 什么是闭包?安全

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

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

什么是域?

在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
  }
})();

BenGa0翻译, 原文连接:http://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/

相关文章
相关标签/搜索