前端进击的巨人(三):从作用域走进闭包

进击的巨人第三篇,本篇就作用域、作用域链、闭包等知识点,一一击破。

作用域

作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符(变量)的访问权限

——《你不知道的JavaScript上卷》

作用域有点像圈地盘,大家划好区域,然后各自经营管理,井水不犯河水。

var globaValue = ‘我是全局作用域‘;
function foo() {
    var fooValue = ‘我是foo作用域‘;
    function bar() {
        var barValue = ‘我是bar作用域‘;
    }
}

function other() {
    var otherValue = ‘我是other作用域‘;
}

作用域的变量声明

不同作用域下命名相同的变量不会发生冲突,"就近原则"选取。

var name = ‘任何名字‘;
function getName() {
    var name = ‘以乐之名‘;
    console.log(name);    // ‘以乐之名‘
}
console.log(name);        // ‘任何名字‘

作用域的类型

执行上下文环境有:全局、函数、eval。那么作用域也有三种,ES6新增了块级作用域。

  1. 全局作用域
  2. 函数作用域
  3. eval作用域(不推荐使用eval,暂时忽略)
  4. 块级作用域(ES6新增)

全局作用域

JavaScript中全局环境只有一个,对应的全局作用域也只有一个。没有用var/let/const声明的变量默认都会成为全局变量。

function foo() {
    a = 10;
};
console.log(a);    // 10 变全局变量(意外由此发生)

函数作用域

ES6之前,想要实现局部作用域的方式,都是是通过在函数中声明变量来实现的,所以也称函数作用域,支持嵌套多个。

var a = 20;
function foo() {
    var a = 10;
    console.log(a);    // 10;
}

函数中声明变量时,建议在函数起始部分声明所有变量,方便查看,切记要用var/let/const声明,防止手抖将局部变量变成成全局变量。

function getClient() {
    var name;
    var phone;
    var sex;
}

块级作用域

我们先来理解什么是块?所谓块,其实就是被大括号{}包裹的代码部分。

if (true) {
    // 这里就是块了,也可称代码块
}

ES6前没有块级作用域的概念,所以{}中并没有自己的作用域。如果我们想在ES5的环境下构建块级作用域,一般都是是通过立即执行函数来实现的。

var name = ‘任何名字‘;
(function(window) {
    var name = ‘以乐之名‘;
    console.log(name);    // ‘以乐之名‘
}(window));
console.log(name);        // ‘任何名字‘

ES5借助函数作用域来实现块级作用域的方式,会让我们的代码充斥大量的立即执行函数(IIFE),不便于代码的阅读。好的代码的就跟好的文章一样,让阅读的人读来舒畅明了。

为此,ES6新增块级作用域的概念,使用let/const声明变量的方式,即可将其作用域指定在代码块中,跟函数作用域一样支持嵌套。

let i = 0;
for (let i = 0; i < 10; i++){
    console.log(i);
}
i;    // 0

let/const不允许变量提升,必须"先声明再使用"。这种限制,称为"暂时性死区"。这也能让我们在代码编写阶段变得更加规范化,执行跟书写顺序保持一致。

作用域链(变量查询规则)

变量被作用域所管理,那么变量在作用域中的查找规则,就是所谓的作用域链。

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问

——《JavaScript高级程序涉及》

"在当前执行环境开始查找使用到的变量,如果找到,则返回其值。如果找不到,会逐层往上级(父作用域)查找,直到全局作用域"

var money = 100;
function foo() {
    function bar() {
        console.log(money);
    }
    bar();
}
foo();

自由变量

变量我们见的不少,但"自由变量"听着是不是挺唬人的。其实对它,我们并不陌生。

"自由变量:当前执行环境使用到,但并未在当前执行环境声明的变量(函数参数arguments排除)"

函数调用时,进入执行上下文创建阶段,会对argument进行隐式的变量声明。

var outer = ‘我是外面变量‘;
function foo() {
    var inner = ‘我是里面变量,不是自由变量‘;
    console.log(outer);
    // 这里用到了outer,但outer并不在函数foo中声明,所以outer就是foo中的自由变量
}

"自由变量的作用域由词法环境决定,也就是它的作用域在代码书写阶段就已经确定了,而不是在代码编译执行阶段确定。"

"自由变量的值是在代码执行时确定的,变量变量变量,值肯定要变,所以自由变量的值只有在程序运行阶段才能确定。"

闭包

开篇第一文我们就执行环境,执行栈做出了详解,有所遗忘的可再温习。执行栈是我们理解闭包原理基础中的基础。

函数调用栈过程的图再晒出来,顺便温习下。

function foo () {
    function bar () {
        return ‘I am bar‘;
    }
    return bar();
}
foo();

函数调用时入栈,调用结束出栈。执行函数时,会创建一个变量对象去存储函数中的变量,方法,参数arguments等,结束调用时,该变量对象就会被销毁。(理想的情况下,不理想的情况就是出现"闭包"调用了)。

什么是闭包?

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

——《JavaScript高级程序设计》

闭包是指那些能够访问自由变量的函数。

——MDN

闭包的特点首先是函数,其次是它可以访问到父级作用域的变量对象,即使父级函数完成调用后"理应出栈销毁"

判定闭包出现

  1. 函数作为参数传递
  2. 函数作为返回值传递
function foo() {
    var fooVal = ‘2019‘;
    var bar = function() {
        console.log(fooVal);    // bar中使用到了自由变量fooVal
    }
    return bar;                 // 函数作为参数返回
}

var getValue = foo();
getValue();                     // 2019

对函数中谁是闭包,各文档解释不一。在此我们遵照Chrome的方式,暂且称foo是闭包。

因为作用域和作用域链规则的限定,子环境的自由变量只能逐层向上到父环境查找。

但是通过闭包,我们在外部环境也可以获取到变量fooVal,虽然foo()函数执行完成了,但它并没从函数调用栈中销毁,其变量对象存储仍然能被访问到。

实际执行过程请看图:

把上述代码改以下,接着看:

function foo() {
 var fooVal = ‘2019‘;
 var bar = function() {
 console.log(fooVal);     // bar中使用到了自由变量fooVal
 }
 return bar;              // 函数作为参数返回
}
var getValue = foo();
var fooVal = ‘2018‘;      // 这里的fooVal是全局作用域的变量
getValue();               // 2019

答案与结果不符的小伙伴要回头理解下自由变量了。"自由变量的作用域在代码书写时(函数创建时)就确定了",所以函数中getValue()使用的fooValfoo的作用域下,而不是在全局作用域下。

答对的小伙伴们再来一道题,加深你的记忆

function fn() {
    var max = 10;
    function bar(x) {
        if (x > max) {
            console.log(x)
        }
    }
    return bar;
}
var f1 = fn();
var max = 100;

f1(20);                 // 输出20

题目解析:max作为函数bar中的自由变量,它的作用域在函数bar创建的时候就确定了,就是函数fn中的max,所以它的作用域链查找到fn中已经结束并返回了,不会再向上找到全局作用域。

注意:栈中存储的不只是闭包中使用到的自由变量,而是父级函数的整个变量对象(父级函数作用域中声明的方法,变量,参数等)

闭包的应用场景

上文中已经阐述了闭包的特点,就是能够让我们跨作用域取值(不局限于父子作用域)。列举两个实际开放中常用的栗子:

  1. 封装回调保存作用域
for(var i = 1; i < 5; i++) {
    setTimeout((function(i){
       return function() {
           console.log(i);
       }
    })(i), i * 1000)
}
// 原理:通过自执行函数传参i,然后返回一个函数(闭包)中使用i,使父函数的变量对象一直存在
  1. 私有变量和方法实现模块化
var makePeople = function () {
    var _name = ‘以乐之名‘;
    return {
        getName: function () {
            console.log(_name);
        },
        setName: function (name) {
            if (name != ‘Hello world‘) {
                _name = name;
            }
        }
    }
}

var me = makePeople();
me.getName();                   // ‘以乐之名‘
me.setName(‘KenTsang‘);
me.getName();                   // ‘KenTsang‘

// 原理:私有变量_name没有对外访问权限,但通过闭包使其一直保留在内存中,可以被外部调用

闭包的应用场景还有很多,具体实际情况还需具体分析。

闭包造成的内存泄露

闭包的使用,破坏了函数的出栈过程。解释执行栈的时候,讲到同个函数即使调用自身,创建的变量对象也并非同一个,其内存存储是各自独立的。

栈中只入不出,函数的变量对象没有被有效回收,就会造成浏览器内存占用逐步增加,内存占用过高的情况下,就会导致页面卡顿,甚至浏览器崩溃。这就是我们常说的闭包造成的"内存泄露"

所以,一名合格的前端,除了会用闭包,还要正确的解除闭包引用。

垃圾回收机制讲解时,通过设置变量值为null时可已解除变量的引用,以便下一次垃圾回收销毁它。

function foo() {
 var fooVal = ‘2019‘;
 var bar = function() {
 console.log(fooVal);
 }
 return bar;
}
var getValue = foo();
var fooVal = ‘2018‘;
getValue();
getValue = null;         // 解除引用,下一次垃圾回收就会回收了

写在结尾

闭包算是前端初学者的一个难点,能解释清除并不容易,涉及到作用域,执行上下文环境、变量对象等等。

零散知识的内聚汇总,正是是系列更文的初衷所在。

知识不是小段子,听完笑过就忘,唯有形成体系,达成闭环,才能深植入记忆中。



参考文档:

本文首发Github,期待Star!

https://github.com/ZengLingYong/blog

作者:以乐之名

本文原创,有不当的地方欢迎指出。转载请指明出处。

原文地址:https://www.cnblogs.com/kenz520/p/10291232.html

时间: 2024-10-10 16:45:56

前端进击的巨人(三):从作用域走进闭包的相关文章

前端进击的巨人(四):略知函数式编程

系列更文前三篇文章,围绕了一个重要的知识点:"函数". 函数调用栈.函数执行上下文.函数作用域到闭包.可见不理解函数式编程,代码都撸不好. 函数是一等公民 函数与其它数据类型一样,可以作为值赋给变量,作为参数传递或返回值返回,也可以像对象一样给函数创建属性(不推荐给函数加属性,虽然可用). 函数在实际开发中应用: 函数声明 函数表达式 匿名函数 自执行函数 // 函数声明 function getName() { //... } // 函数表达式 var getName = funct

20170917 前端开发周报:JavaScript函数式编程、作用域和闭包

1.用函数式编程对JavaScript进行断舍离 当从业20的JavaScript老司机学会函数式编程时,他扔掉了90%的特性,也不用面向对象了,最后发现了真爱啊!!! https://juejin.im/entry/59b86... 2.JavaScript作用域和闭包 作用域和闭包在JavaScript里非常重要.但是在我最初学习JavaScript的时候,却很难理解.这篇文章会用一些例子帮你理解它们.我们先从作用域开始.作用域 JavaScript的作用域限定了你可以访问哪些变量.有两种作

前端知识体系:JavaScript基础-作用域和闭包-闭包的实现原理和作用以及堆栈溢出和内存泄漏原理和相应解决办法

闭包的实现原理和作用 闭包: 有权访问另一个函数作用域中的变量的函数. 创建闭包的常见方式就是,在一个函数中创建另一个函数. 闭包的作用: 访问函数内部变量.保持函数在环境中一直存在,不会被垃圾回收机制处理 因为函数内部声明 的变量是局部的,只能在函数内部访问到,但是函数外部的变量是对函数内部可见的,这就是作用域链的特点了. 子级可以向父级查找变量,逐级查找,找到为止 因此我们可以在函数内部再创建一个函数,这样对内部的函数来说,外层函数的变量都是可见的,然后我们就可以访问到他的变量了. <scr

js 作用域,闭包及其相关知识的总结

面试必问题,闭包是啥有啥子用,觉得自己之前回答的并不好,所以这次复习红皮书的时候总结一下. 提到闭包,相关的知识点比较多,所以先罗列一下要讲的内容. 1. 作用域链,活动对象 2. 关于this对象 3. 垃圾回收机制,内存泄漏 4. 模仿块级作用域,私有变量 涉及的内容这么多,也难怪面试官喜欢问这个问题啊,就像niko大神说的,应该是根据回答的深浅了解你的思维模式吧.废话不多说,开始步入正题. 1. 作用域链,活动对象 活动对象:活动对象就是在函数第一次调用时,创建一个对象,在函数运行期是可变

前端进击的巨人(二):栈、堆、队列、内存空间

面试经常遇到的深浅拷贝,事件轮询,函数调用栈,闭包等容易出错的题目,究其原因,都是跟JavaScript基础知识不牢固有关,下层地基没打好,上层就是豆腐渣工程,新人小白,踏实踩土才是关键. 打地基第二篇:本篇我们将对JavaScript数据结构的知识点详解一二. JavaScript中有三种数据结构: 栈(stack) .堆(heap). 队列(queue). 栈(stack) 栈的特点是"LIFO,即后进先出(Last in, first out)".数据存储时只能从顶部逐个存入,取

前端进击的巨人(六):知否知否,须知this

常见this的误解 指向函数自身(源于this英文意思的误解) 指向函数的词法作用域(部分情况) this的应用环境 全局环境 无论是否在严格模式下,全局执行环境中(任何函数体外部)this都指向全局对象 var name = '以乐之名'; this.name; // 以乐之名 函数(运行内)环境 函数内部,this的值取决于函数被调用的方式(被谁调用) var name = '无名氏'; function getName() { console.log(this.name); } getNa

JavaScript函数,作用域以及闭包

JavaScript函数,作用域以及闭包 1. 函数 (1). 函数定义:函数使用function关键字定义,它可以用在函数定义表达式或者函数声明定义. a. 函数的两种定义方式: * function functionName() {} * var functionName = function(){} b. 两种函数定义不同之处 1). 声明提前问题 函数声明语句   :声明与函数体一起提前 函数定义表达式 :声明提前,但是函数体不会提前 请看下面图示:绿色线上面实在js初始加载的时候,查看

JavaScript【5】高级特性(作用域、闭包、对象)

笔记来自<Node.js开发指南>BYVoid编著 1.作用域 if (true) { var somevar = 'value'; } console.log(somevar); JavaScript的作用域完全是由函数决定的,if.for语句中的花括号不是独立的作用域. 1.1.函数作用域 作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,我们称为函数作用域.在函数中引用一个变量时,JavaScript会先搜索当前函数作用域,或者称为"局部作用域",

js最基础知识回顾3(字符串拼接,数据类型,变量类型,变量作用域和闭包,运算符,流程控制,)

一.javaScript组成     1.ECMAScript:解释器.翻译 ---------------------------------------------------------几乎没有兼容性问题     2.DOM:Document Object Model --------操作HTML的能力----document--------有一些兼容性问题     3.BOM:Browser Object Model -------------浏览器---------------wind