JS基础知识总结(四):作用域与闭包

本文是JS基础知识总结的第四篇文章,整个JS基础知识总结系列的文章可戳:

一.变量类型

  • 原始类型、复合类型、类型判断
  • 数据类型转换
  • 值类型和引用类型

二.浅拷贝与深拷贝

  • 浅拷贝的实现方式
  • 深拷贝的实现方式
  • 与赋值的区别

三.原型与原型链

  • 原型
  • 原型链的定义

四.作用域与闭包

  • 作用域与作用域链
  • 变量提升、函数提升
  • 闭包

1.作用域

回顾下jQuery源码中,代码是包在(function(){//代码})()当中。主要的目的是希望里面的所有变量,不会暴露到外面,以防止变量全局污染,这就是函数作用域。

在JS中,变量的作用域有两种:全局作用域和函数作用域。ES6中有了块级作用域的概念,使用let、const定义变量即可。

1.1全局作用域

(1)没有用var声明的变量(除去函数的参数)都具有全局作用域,成为全局变量;

(2)window的所有属性都具有全局作用域;

(3)最外层函数体外声明的变量也具有全局作用域

最外层的作用域,具有全局作用域的变量可以被任何函数访问。

            var data = {name:"peter"};
            (function f1(){
              console.log('data',data);//{name:"peter"}
            })();

这样的坏处就是变量间很容易产生冲突。比如开发者A定义一个变量,开发者B又定义了一个同名变量:

var data = {name:"peter"};//A定义
//省去100行代码
var data = {name:"lily"};//B定义

这样变量就很容易冲突。

1.2函数作用域

在函数作用域中定义的变量,在函数外部是无法访问的,会报错:

function f1(){
    var data={name:"peter"}
}
console.log('data',data)//Uncaught ReferenceError: data is not defined

1.3ES6中的块级作用域

什么是块级作用域呢?

任何一个对花括号({})中的语句都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,称之为块级作用域。

let所声明的变量,只在使用let所在的代码块内有效,在代码块外调用let声明的变量则会报错:

示例:

{
    var a={name:"peter"}
    let b={name:"lily"}
    console.log('inner a',a)//{name:"peter"}
    console.log('inner b',b)//{name:"lily"}
}
console.log('a',a)//a{name:"peter"}
console.log('b',b)//Uncaught ReferenceError: b is not defined

说明:

(1)不存在变量提升,使用let命令声明的变量,只能在声明后使用,语法上称为“暂时性死区”

(2)使用const声明的变量不能再次赋值。

2.变量提升、函数提升

示例:

console.log('f1',f1);
function f1(){
    console.log('enter f1')
}
var f1 = 1;

结果:

从结果可以看出打印的f1是函数,不是undefined,也不是后面变量声明的1。这说明: 函数提升优先于变量提升,会优先处理函数声明,再处理变量声明。

若声明的变量和声明的函数重名,则变量的声明不会影响函数的声明。

小结:

(1)使用var声明的变量存在变量提升,即可在声明变量之前使用变量。使用let、const声明的变量不能在声明变量之前使用变量。

(2)函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升会把变量声明挪到作用域顶部。

3.作用域链

要得到一个变量的值,若当前作用域没有定义,就到父级作用域寻找。如果父级作用域中也没找到,就再向一层去寻找,直到找到全局作用域。这种一层一层的关系,就是作用域链。

示例:

            var data = { name: "peter" };
            (function f1() {
                var a = 1;
                (function f2() {
                    var b = 2;
                    console.log('data', data)//{name:"peter"},data向上顺着作用域寻找
                    console.log('a', a)//a向上顺着作用域寻找
                    console.log('b', b)//b是当前作用域的变量
                    console.log('c', c)
                })();//立即执行f2
            })();//立即执行f1

4.闭包

闭包:有权访问另一个作用域中的变量的函数。

换个直观的说法,在函数A内部有个函数B,函数B可以访问函数A中的变量,函数B就是闭包:

           function A(){
                var a = 1;
                B = function(){
                    console.log('a in function B',a)
                }
            }
            A();
            B();//执行B,结果是:1    

优点:

可以避免全局变量的污染

缺点:

参数和变量不会被垃圾回收机制回收,闭包会常驻内存,增大内存使用率,使用不当容易造成内存泄漏。

关于闭包有个经典问题,就是循环中使用闭包解决用var定义变量的问题,下面有两个示例:

示例1:

            for (var i = 1; i <= 8; i++) {
                setTimeout(function () {
                    console.log(i)//结果:每秒输出一个9,共输出8个9
                }, i * 1000)
            }    

结果:

控制台打印的结果并不是原本以为的,按秒输出1,2,3,...,7,8,。而是按秒输出9,9,9,...,9,9,共输出8个9。

原因是setTimeout()是一个异步函数,会先把循环执行完,所以i的值是9,然后再按1秒一个,共输出8个9。

类似的问题还有给下面的每个<li>加个点击事件,点击对应的<li>就弹出对应<li>中的内容,示例二:

        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
        </ul>            

js:

            var aLi = document.getElementsByTagName('li');
            for(var i = 0;i<aLi.length;i++){
                aLi[i].addEventListener('click',function(){
                    alert(i+1)
                })
            }

其实执行后会发现,不管点击哪个<li>,弹出的都是7。

上述两个例子的结果都不符合预期,解决的方法是可以通过闭包来解决:

针对示例一的解决方法:

         for (var i = 1; i <= 8; i++) {
                ; (function (j) {
                    setTimeout(function () {
                        console.log(j)//结果:按秒输出1,2,3,...,8
                    }, j * 1000)
                })(i)
            }

当然除了闭包的解决方法,最简单的解决方法是使用let替代var来定义i:

        for (let i = 1; i <= 8; i++) {
                setTimeout(function () {
                    console.log(i)//结果:按秒输出1,2,3,...,8
                }, i * 1000)
            }            

针对示例二的解放方法:

var aLi = document.getElementsByTagName('li');
            for (var i = 0; i < aLi.length; i++) {
                ; (function (j) {
                    aLi[j].addEventListener('click', function () {
                        alert(j + 1)
                    })
                })(i)
            }

闭包的基本知识可参考:

学习Javascript闭包(Closure)

MDN

5.小结

本文主要分别介绍了作用域、作用域链、变量提升和闭包的内容,作为JS基础知识总结的最后一篇。如有问题,欢迎指正。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏闪电gogogo的专栏

《统计学习方法》笔记七(3) 支持向量机——非线性支持向量机

13020
来自专栏python学习之旅

算法笔记(九):希尔排序和桶排序

先将整个待排记录序列分割成若干个子序列,然后分别进行直接插入排序,待整个序列中的数据基本有序时,再对全体记录进行一次直接插入排序。具体做法是:

10620
来自专栏苦逼的码农

【算法实战】生成窗口最大值数组

做算法题了,题的难度我们分为“士,尉,校,将”四个等级。这个算法题的模块是篇幅比较小的那种模块。首先是给出一道题的描述,之后我会用我的想法来做这道题,今天算是算...

14920
来自专栏iOSDevLog

机器学习五大流派

19220
来自专栏智能相对论

为什么这两家娱乐公司都在不务正业搞AI竞赛?

市场一直在变,而在公众认知范围内,视频行业的竞争点在内容、在流量、在资金。直到近期,爱奇艺AI竞赛“发榜”,市场惊觉在竞争格局越发紧张的现在,在线视频已经在凭技...

13030
来自专栏mwangblog

几种分布估计算法介绍

8520
来自专栏mwangblog

分布估计算法求解0-1背包问题一

0-1背包问题是:有一个固定容量的背包,和固定种类的物品,每种物品只有一件。每件物品有各自的价值和重量,求解哪些物品放入背包可以使价值总和最大,且不超过背包容量...

10210
来自专栏DT数据侠

中国“智”造

11月末,入冬的广东并没有太多北方的凉意,前几年一直喊着的“加快推动制造业转型升级”也不只是一个口号,有一群人正在做着。他们中有人是技术极客,有人是科大少年班出...

11720
来自专栏Python数据科学

【算法面经】:机器学习面试算法梳理

机器学习算法面试一直是大家比较苦恼的事情,各种算法经常弄混,或者无法透彻理解。分享一篇非常好的机器学习算法面试干货总结,梳理算法原理,优缺点。

15920
来自专栏JavaQ

高并发编程-HashMap深入解析

在JDK1.8以前版本中,HashMap的实现是数组+链表,它的缺点是即使哈希函数选择的再好,也很难达到元素百分百均匀分布,而且当HashMap中有大量元素都存...

13420

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励