从作用域链谈闭包

闭包(closure)是Javascript语言的一个难点,也是它的特色。非常多高级应用都要依靠闭包实现。

神马是闭包

关于闭包的概念,是婆说婆有理。

因而,我就翻阅了红皮书(p178)上对于闭包的陈述:

闭包是指有权訪问另外一个函数作用域中的变量的函数

这概念有点绕,拆分一下。从概念上说,闭包有两个特点:

1、函数

2、能訪问另外一个函数作用域中的变量

在ES 6之前,Javascript仅仅有函数作用域的概念。没有块级作用域(但catch捕获的异常 仅仅能在catch块中訪问)的概念(IIFE能够创建局部作用域)。每一个函数作用域都是封闭的,即外部是訪问不到函数作用域中的变量。

function getName() {
  var name = "美女的名字";
  console.log(name);     //"美女的名字"
}
function displayName() {
    console.log(name);  //报错
}

可是为了得到美女的名字,不死心的单身汪把代码改成了这样:

function getName() {
  var name = "美女的名字";
  function displayName() {
    console.log(name);
  }
  return displayName;
}
var 美女 = getName();
美女()  //"美女的名字"

这下,美女是一个闭包了,单身汪想怎么玩就怎么玩了。(但并不推荐单身汪用中文做变量名的写法。大家不要学)。

关于闭包呢。还想再说三点:

1、闭包能够訪问当前函数以外的变量

function getOuter(){
  var date = ‘815‘;
  function getDate(str){
    console.log(str + date);  //訪问外部的date
  }
  return getDate(‘今天是:‘); //"今天是:815"
}
getOuter();

getDate是一个闭包,该函数运行时,会形成一个作用域A。A中并未定义变量date。但它能在父一级作用域中找到该变量的定义。

2、即使外部函数已经返回。闭包仍能訪问外部函数定义的变量

function getOuter(){
  var date = ‘815‘;
  function getDate(str){
    console.log(str + date);  //訪问外部的date
  }
  return getDate;     //外部函数返回
}
var today = getOuter();
today(‘今天是:‘);   //"今天是:815"
today(‘明天不是:‘);   //"明天不是:815"

3、闭包能够更新外部变量的值

function updateCount(){
  var count = 0;
  function getCount(val){
    count = val;
    console.log(count);
  }
  return getCount;     //外部函数返回
}
var count = updateCount();
count(815); //815
count(816); //816

作用域链

为毛闭包就能訪问外部函数的变量呢?这就要说说Javascript中的作用域链了。

Javascript中有一个运行环境(execution context)的概念,它定义了变量或函数有权訪问的其它数据。决定了他们各自的行为。

每一个运行环境都有一个与之关联的变量对象。环境中定义的全部变量和函数都保存在这个对象中。你能够把它当做Javascript的一个普通对象。可是你仅仅能改动它的属性,却不能引用它。

变量对象也是有父作用域的。

当訪问一个变量时,解释器会首先在当前作用域查找标示符,假设没有找到,就去父作用域找。直到找到该变量的标示符或者不再存在父作用域了。这就是作用域链。

作用域链和原型继承有点相似,但又有点小差别:假设去查找一个普通对象的属性时,在当前对象和其原型中都找不到时,会返回undefined。但查找的属性在作用域链中不存在的话就会抛出ReferenceError。

作用域链的顶端是全局对象。

对于全局环境中的代码,作用域链仅仅包括一个元素:全局对象。所以。在全局环境中定义变量的时候,它们就会被定义到全局对象中。

当函数被调用的时候,作用域链就会包括多个作用域对象。

全局环境

关于作用域链讲得略多(红皮书上有关于作用域及运行环境的详解)。看一个简单地样例:

// my_script.js
"use strict";
var foo = 1;
var bar = 2;

在全局环境中,创建了两个简单地变量。

如前面所说,此时变量对象是全局对象:

运行上述代码,my_script.js本身会形成一个运行环境。以及它所引用的变量对象。

Non-nested functions

改动一下代码。创建一个没有函数嵌套的函数:

"use strict";
var foo = 1;
var bar = 2;
function myFunc() {
  //-- define local-to-function variables
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
}
console.log("outside");
//-- and then, call it:
myFunc();

当myFunc被定义的时候,myFunc的标识符(identifier)就被加到了当前的作用域对象中(在这里就是全局对象),而且这个标识符所引用的是一个函数对象(function object)。函数对象中所包括的是函数的源码以及其它的属性。

当中一个我们所关心的属性就是内部属性[[scope]]。[[scope]]所指向的就是当前的作用域对象。也就是指的就是函数的标识符被创建的时候,我们所能够直接訪问的那个作用域对象(在这里就是全局对象)。

比較重要的一点是:myFunc所引用的函数对象,其本身不仅仅含有函数的代码,而且还含有指向其被创建的时候的作用域对象

当myFunc函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包括myFunc函数所定义的本地变量,以及其參数(arguments)。

这个新的作用域对象的父作用域对象就是在运行myFunc时我们所能直接訪问的那个作用域对象。

所以。当myFunc被运行的时候,对象之间的关系例如以下图所看到的:

Nested functions

如前面所说,当函数返回没有被引用的时候,就会被垃圾回收器回收。可是对于闭包(函数嵌套是形成闭包的一种简单方式)呢,即使外部函数返回了,函数对象仍会引用它被创建时的作用域对象。

"use strict";
function createCounter(initial) {
  var counter = initial;
  function increment(value) {
    counter += value;
  }
  function get() {
    return counter;
  }
  return {
    increment: increment,
    get: get
  };
}
var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100
myCounter.increment(5);
console.log(myCounter.get());   // 返回 105

当调用createCounter(100)时,对象之间的关系例如以下图所看到的:

内嵌函数increment和get都有指向createCounter(100) scope的引用。

假设createCounter(100)没有不论什么返回值,那么createCounter(100) scope不再被引用。于是就能够被垃圾回收。

可是由于createCounter(100)实际上是有返回值的,而且返回值被存储在了myCounter中,所以对象之间的引用关系变成了例如以下图所看到的:

须要用点时间思考的是:即使createCounter(100)已经返回,可是其作用域仍在,并能且仅仅能被内联函数訪问。能够通过调用myCounter.increment() 或 myCounter.get()来直接訪问createCounter(100)的作用域。

当myCounter.increment() 或 myCounter.get()被调用时。新的作用域对象会被创建。而且该作用域对象的父作用域对象会是当前能够直接訪问的作用域对象。

此时,引用关系例如以下:

当运行到return counter;时,在get()所在的作用域并没有找到相应的标示符,就会沿着作用域链往上找,直到找到变量counter,然后返回该变量。

调用increment(5)则会更有意思:

当单独调用increment(5)时,參数value会存贮在当前的作用域对象。函数要訪问value。能立即在当前作用域找到该变量。可是当函数要訪问counter时,并没有找到,于是沿着作用域链向上查找。在createCounter(100)的作用域找到了相应的标示符。increment()就会改动counter的值。

除此之外。没有其它方式来改动这个变量。闭包的强大也在于此。能够存贮私有数据。

Similar function objects, different scope objects

对于上面的counter演示样例,再说点扩展的事。看代码:

//myScript.js
"use strict";
function createCounter(initial) {
  /* ... see the code from previous example ... */
}
//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);

myCounter1 和 myCounter2创建之后。关系图是酱紫的:

在上面的样例中。myCounter1.increment和myCounter2.increment的函数对象拥有着一样的代码以及一样的属性值(name,length等等),可是它们的[[scope]]指向的是不一样的作用域对象。

这才有了以下的结果:

var a, b;
a = myCounter1.get();   // a 等于 100
b = myCounter2.get();   // b 等于 200
myCounter1.increment(1);
myCounter1.increment(2);
myCounter2.increment(5);
a = myCounter1.get();   // a 等于 103
b = myCounter2.get();   // b 等于 205

作用域和this

作用域会存储变量。但this并非作用域的一部分,它取决于函数调用时的方式。

时间: 2024-10-11 03:43:17

从作用域链谈闭包的相关文章

JavaScript中的作用域链和闭包

JavaScript中的作用域链和闭包 2012-06-29 11:41 1878人阅读 评论(46) 收藏 举报 JavaScript中出现了一个以前没学过的概念——闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥

作用域链、闭包和原型链

Function:  匿名函数,作用域,作用域链和闭包 函数的重载:  什么是:函数名相同,参数列表不同.根据传入函数的参数的不同,整形不同的逻辑. 何时用:如果一项任务,根据不同的参数,不执行不用的逻辑. 优点:减轻调用者的负担.  问题:js语法不知函数的重载. 解决办法:在函数中都有arguments的属性,专门用于接收传入函数的所有参数值的类数组对象. 匿名函数:  什么是:在函数定义时,不给函数名,即不被任何变量引用. 何时用:确定函数只使用一次.  优点:节约内存. 如何用:1.自调

个人理解的javascript作用域链与闭包

闭包引入的前提个人理解是为从外部读取局部变量,正常情况下,这是办不到的.简单的闭包举例如下: 1 function f1(){ 2 3 n=100; 4 5 function f2(){ 6 alert(n); 7 } 8 9 return f2; 10 11 } 12 13 var result=f1(); 14 15 result(); // 100 代码中的f2函数,就是闭包. 1 function f1(){ 2 3 var n=100; 4 5 nAdd=function(){n+=1

JS详细图解作用域链与闭包

JS详细图解作用域链与闭包 攻克闭包难题 初学JavaScript的时候,我在学习闭包上,走了很多弯路.而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战. 闭包有多重要?如果你是初入前端的朋友,我没有办法直观的告诉你闭包在实际开发中的无处不在,但是我可以告诉你,前端面试,必问闭包.面试官们常常用对闭包的了解程度来判定面试者的基础水平,保守估计,10个前端面试者,至少5个都死在闭包上. 可是为什么,闭包如此重要,还是有那么多人没有搞清楚呢?是因为大家不愿意学习吗?还真不是,

在chrome开发者工具中观察函数调用栈、作用域链与闭包

在chrome开发者工具中观察函数调用栈.作用域链与闭包 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化.因此,断点调试对于快速定位代码错误,快速了解代码的执行过程有着非常重要的作用,这也是我们前端开发者必不可少的一个高级技能. 当然如果你对JavaScript的这些基础概念[执行上下文,变量对象,闭包,this等]了解还不够的话,想要透彻掌握断点调试可能会有一些困

作用域链与闭包

读了这篇博文地址后,对作用域和闭包终于有一了些了解.之前看各种文章,让我以为闭包是因为内部引用变量,导致变量无法在外部访问,而通过内部函数可以被外部访问导致被引用的变量可以间接访问.现在看来,这只能说是闭包的一种外在表现,跟闭包本身没有任何关系的. 下面总结一下作用域链和闭包. 1. 执行至闭包之前的整个过程 更为详细的可参见那篇博文,这里只是简单总结下.在浏览器执行解析代码时,分为编译和代码执行两个部分.编译阶段负责对语法解析,作用域链分析,语法优化.代码执行阶段中,先为函数创建上下文,然后执

js内存空间 执行上下文 变量对象详解 作用域链与闭包 全方位解读this

内存空间:https://blog.csdn.net/pingfan592/article/details/55189622 执行上下文:https://blog.csdn.net/pingfan592/article/details/55189804 变量对象详解:https://blog.csdn.net/pingfan592/article/details/56009330 作用域链与闭包:https://blog.csdn.net/pingfan592/article/details/5

1--面试总结-js深入理解,对象,原型链,构造函数,执行上下文堆栈,执行上下文,变量对象,活动对象,作用域链,闭包,This

参考一手资料:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/中文翻译版本:https://zhuanlan.zhihu.com/p/32042645 Javascript 是一种单线程编程语言,这意味着它只有一个调用栈,call Stack(调用栈 ,,先入后出) 核心:对象,原型链,构造函数,执行上下文堆栈,执行上下文,变量对象,活动对象,作用域链,闭包,This js原型链? 定义 原型对象也是简单的对象并且可以拥有它们自

JavaScript中的作用域 、作用域链和闭包

JavaScript中作用,作用域链和闭包详解 一.作用域在js中有全局变量和局部变量之分:比如var a = 1;function sum(){var b=1console.log(b) //1console.log(a) //2 }sum()console.log(a) //3console.log(b) //4 例子中 a 是全局变量,b是局部变量(定义在函数内部,只能在函数内部访问)所以第1行正确 函数内部也能访问全局变量 a所以第2行也能正确 第三行也正确.第4行有外部不能访问内部变量