作用域链与闭包

1.作用域链

1.1 作用域链是什么?

作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。
首先,代码在其对应的环境中执行时,数据是保存在其变量对象中的。但是有时会使用其范围外的数据。当需要查找变量的值时,现将变量解析,然后从链表的第一项开始寻找,找到为止。

1.2 作用域链的形成

全局环境中,作用域链由一个全局对象组成。
而函数中,作用域链的形成分为两个过程
(1)函数创建时:会创建一个预先包含全局变量对象的作用域链,被保存在内部的【【scope】】属性中。
(2)当调用函数时,会为函数创建一个执行环境,然后通过复制函数的【【scope】】属性中的对象构建起执行环境的作用域链。然后又有一个活动对象被创建并推入执行作用域链的最前端。

即函数作用域链 = AO + scope

1.3 作用域链形成的实际列子

引用汤姆大叔的文章

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); // 60

全局上下文的变量对象是:

globalContext.VO === Global = {
x: 10
foo: <reference to function>
};

在“foo”创建时,“foo”的[[scope]]属性是:

foo.[[Scope]] = [
globalContext.VO
];

在“foo”激活时(进入上下文),“foo”上下文的活动对象是:

fooContext.AO = {
y: 20,
bar: <reference to function>
};

“foo”上下文的作用域链为:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];

内部函数“bar”创建时,其[[scope]]为:

bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];

在“bar”激活时,“bar”上下文的活动对象为:

barContext.AO = {
z: 30
};

“bar”上下文的作用域链为:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];

1.4 此过程中闭包的形成

函数(调用时),会创建一个执行环境及相应的作用域链,然后使用arguments和其他命名参数的值来初始化函数的活动对象(AO),但在作用域链中,外部函数的活动对象始终处于第二位,外部的外部处于第三位,以此类推。
执行环境有一个表示变量的对象--变量对象(VO),全局环境的VO始终存在,函数这样的局部环境的VO只在函数的执行过程中存在。
内部函数中将会将外部函数的活动对象添加到其作用域链中,当外部函数执行完毕后,其执行环境的作用域链会被销毁,而活动对象会仍然保留在内存中。

由于作用域链的形成机制,导致了某些函数在执行完毕后,并不会销毁其活动对象,因为其被包含在了其他函数的作用域链中。

2. 闭包

可以以正常数据形式存在的函数(比方说:当参数传递,接受函数式参数或者以函数值返回)都称作 第一类函数(一般说第一类对象)。在ECMAScript中,所有的函数都是第一类对象。

在函数中,如果包含了自由变量(并未在函数内部声明),那么即产生了闭包。因为它涉及到了另外一个环境的数据,那么会将另外一个环境中的VO暂存起来。

2.2 闭包实战

2.2.1如下代码输出多少?如果想输出3,那如何改造代码?

var fnArr = [];
for (var i = 0; i < 10; i ++) {
  fnArr[i] =  function(){
    return i
  };
}
console.log( fnArr[3]() )

分析:代码的本意是想,fnArr3中,是调用的第数组中的四个函数,并且函数范围值与数组下标是一样的。这里会直接输出10,因为数组中函数仅仅是声明了,I并未赋值,等到调用时,其作用域中并不包含I,就向上级寻找I,而此时所有的函数都是共享一个父作用域,I的值为10.
如果要输出3,就是让每一次为数组赋值时,能够让函数能够记住此时i的值,并在调用的时候能够取到。
所以我们将循环更改一下:
for (var i = 0; i < 10; i ++) {
(function(j){
fnArr[j] = function(){
return j
};
})(i);
}
这里我们将赋值过程用一个立即执行函数封装了起来,并且通过参数传值,将i的值保存在了外层立即执行函数的执行上下文中。

2.2.2 封装一个 Car 对象

var Car = (function(){
    var speed = 0;
    function set(speed1){
        speed = speed1
        console.log(speed)
    }
    function get(){
        console.log(speed)
        return speed;
    }
    function speedUp(){
        speed++;
        console.log(speed)
    }
    function speedDown(){
        speed--;
        console.log(speed)
    }
    return {
        set: set,
        get: get,
        speedUp: speedUp,
        speedDown: speedDown
    }
})()
Car.set(30)
Car.get() //30
Car.speedUp()
Car.get() //31
Car.speedDown()
Car.get()  //30

这里用一个立即执行函数,并返回我们需求的对象的形式来封装。这样的好处是所有的数据都被封装在了立即执行函数中,不会暴露在外面。

2.2.3 如下代码输出多少?如何连续输出 0,1,2,3,4

for(var i=0; i<5; i++){
  setTimeout(function(){
    console.log('delayer:' + i )
  }, 0)
}

分析:setTimeout外层创建一个闭包

for(var i=0; i<5; i++){
    (function (i){
    setTimeout(function(){
        console.log('delayer:' + i )
    }, 0)
    })(i)
}

2.2.4 如下代码输出多少?

function makeCounter() {
  var count = 0

  return function() {
    return count++
  };
}

var counter = makeCounter()
var counter2 = makeCounter();

console.log( counter() ) // 0
console.log( counter() ) // 1

console.log( counter2() ) // 0
console.log( counter2() ) // 1

分析:makeCounter都是返回了一个函数。且couter
与couter2所返回的函数是同名的。但是每次调用时,进入执行上下文,然后绑定了新的作用域链了。

2.2.5 补全代码,实现数组按姓名、年纪、任意字段排序

var users = [
    { name: "John", age: 20, company: "Baidu" },
    { name: "Pete", age: 18, company: "Alibaba" },
    { name: "Ann", age: 19, company: "Tecent" }
]
function byField(string){
    return function(user1,user2){
        return user1[string] >user2[string]
    }
}
users.sort(byField('age'))

2.2.6 写一个 sum 函数,实现如下调用方式

console.log( sum(1)(2) ) // 3
console.log( sum(5)(-1) ) // 4

解答:

function sum(val1){
  return function(val2){
    return val1+val2;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容

  • 作用域链 作用域(scope)作用域是程序源代码中定义变量的区域,规定了当前执行代码对变量和函数可访问的范围。ES...
    LeoCong阅读 203评论 0 0
  • 闭包是js中一个极为NB的武器,但也不折不扣的成了初学者的难点。因为学好闭包就要学好作用域,正确理解作用域链,然而...
    faremax阅读 397评论 0 1
  • 来源:仗剑走天涯! 关于javascript的作用域的一些总结,主要参考以上文章,加上自己的整理的理解。 近日对j...
    Michael_林阅读 930评论 0 1
  • 作用域 在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子...
    Stago阅读 153评论 0 0
  • “莉比斯,你回来啦?” “嗯,维恩今天过得还好吗?” “还好。南部的战争怎么样了?” “别担心,快结束了。明天你就...
    青栀阅读 251评论 0 0