什么是作用域?

就像孙悟空给唐僧画个圈圈一样,这个圈圈就可以称之为作用域,这个比喻可能不太形象。

作用域和孙悟空的圈圈还是有点区别,作用域内部可以获得作用域外部的变量,而内部的变量无法逃逸到作用域外面,如果逃逸出去了,那就造成内存泄漏了,程序将会出现崩溃!

全局作用域

可以理解为就是放在 JS 最外层的那部分内容,比如:变量、函数、对象等等。凡是定义在最外层的内容,都是属于全局作用域,在全局作用域下的任意函数都可访问到这部分内容。

var wechat = '前端路引';

(function () {
function test1 () {
console.log(wechat);
}
test1(); // 输出 '前端路引'
})()

以上代码用到了自执行函数 (function () {})(),作用就是为了创建一个局部作用域,避免变量污染全局作用域,在很多优秀的插件中都能看到它的影子。

上面代码中的 wechat 变量,就是全局作用域下的变量,test1 函数定义在全局作用域内部,所以对于 test1 函数来说,全局作用域中的变量它都是可以访问的。

函数作用域

也可称之为 局部作用域,生效范围在函数内部,在函数外面无法访问。

function test2 () {
var wechat = '前端路引';
console.log(wechat);
}
test2();
console.log(wechat); // 报错:wechat is not defined

wechat 变量定义在函数内部,便是函数作用域,在函数外面无法访问,这就是局部作用域的特性。

块级作用域

ES6 新增的玩法,一对花括号圈出来的区域,就称之为块级作用域。需注意 var 声明的变量是不存在块级作用域的,只有 letconst 才存在块级作用域。

{
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 输出:前端路引
console.log(wechat2); // 报错:wechat2 is not defined
console.log(wechat3); // 报错:wechat3 is not defined

或者是像 if 条件判断的花括号一样也存在块级作用域:

if (true) {
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 输出:前端路引
console.log(wechat2); // 报错:wechat2 is not defined
console.log(wechat3); // 报错:wechat3 is not defined

当然其他 while、for、do 等循环语句也存在块级作用域。

作用域链

作用域链总是从内部开始,一圈一圈往外部查找,比如:

let globalVal = '全局';
function outer() {
let outerVal = '外部';
function inner() {
let innerVal = '内部';
console.log(innerVal); // '内部'(当前作用域)
console.log(outerVal); // '外部'(外层作用域)
console.log(globalVal); // '全局'(全局作用域)
console.log(wechat); // 报错:ReferenceError: wechat is not defined
}
inner();
}
outer();

当内部找不到的时候,就往外一层查找,外层找不到就在全局作用域找,如果全局作用域也找不到,就会报错 ReferenceError

闭包使用

基于作用域的特性,就有前辈们发现了 闭包 的用法,闭包这个东东,用得好呢可以说是一把利剑,用得不好那就要反噬主人了。

闭包 的用处就是搭建函数内部和外部的桥梁,使函数外部可以访问到函数内部的变量。

闭包的基本样子

function test1 () {
const wechat = '前端路引';
function test2 () {
console.log(wechat);
}
return test2;
}
test1()(); // 输出:前端路引

上面代码中 wechat 定义在函数内部,属于函数作用域,test2 也定义在函数内部,使用 test2 访问 wechat 变量的这种方法,就称之为 闭包

为什么需要调用 test1 需要 ()() ?这个只是一种简写,其完整写法应该是这样的:

const temp = test1(); // 获得 test1 返回的函数
temp(); // 执行返回函数输出:'前端路引'

解决循环中的陷阱

在 ES6 出现之前,var 没有块级作用域这个特性,所以循环语句中常常会出现一些坑,比如:

for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出:3 3 3
}, 100)
}

上面代码会输出三次 3,原因是 var 没有块级作用域,setTimeout 函数执行时候,获得的是 for 循环之后的 i 值,所以最终输出都是 3。

使用 let 优化:

for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出:0 1 2
}, 100)
}

let 的块级作用域可以完美保存每次 i 的值,所以最终输出是 0 1 2,这也相当于一种闭包的用法。

使用闭包优化:

for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 输出:0 1 2
}, 100)
})(i)
}

将 i 以函数参数的形式传入,这样每次循环后,函数内部获得的 j 都是当时的 i 值,所以最终输出是 0 1 2。

上面代码可能难以理解,那么换一种写法看看:

function temp (j) {
setTimeout(function () {
console.log(j); // 输出:0 1 2
}, 100)
}
for (var i = 0; i < 3; i++) {
temp(i)
}

这样写是否一眼就懂了?

(function (j) {})(i) 这种写法就相当于一个自执行函数,这个函数有一个参数 j,每次执行的时候传入 i 值而已。

为什么要一个小括号把 function (j) {} 包起来呢?

如果直接写成 function (j) {}(i),JS 解析器没办法识别这是一个函数调用,所以需要用小括号括起来。也可以写成 !function (j){}(i) ,也是自执行函数的一种方式。其他的一元运算符都可以用来这么玩,比如:

+function (j) {}(i)
-function (j) {}(i)
~function (j) {}(i)

个人觉得还是小括号比较容易理解。

私有变量

模块化开发的时候,可以使用闭包封装内部的私有变量,这样外部就无法直接访问,以保证私有变量安全,比如:

const counter = (function() {
let count = 0; // 私有变量
return {
increment: () => count++,
getCount: () => count,
};
})(); counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(无法直接访问)

函数柯里化

闭包的又一种使用形式,柯里化就是把接受多个参数的函数变换成接受一个单一参数的函数。如下:

function add(a) {
return function(b) {
return a + b;
};
} const add5 = add(5); // 返回一个闭包,记住 a=5
console.log(add5(3)); // 8

内存泄漏

由于闭包中的变量会常驻内存,如果不及时释放闭包,那么就会造成内存泄漏,比如:

function createHeavyObj() {
const bigData = new Array(1000000).fill('*'); // 生成一个大对象
return () => bigData; // 闭包引用 bigData
} let fn = createHeavyObj();
// 即使不再需要 bigData,它仍被闭包引用,无法被回收
// 解决方法:手动解除引用
fn = null; // 解除闭包对 bigData 的引用

如果没有 fn = null 这句代码,那么 bigData 会一直存在(直到页面刷新或者被垃圾回收机制回收),如果 createHeavyObj 有多个地方调用,那么就可能导致内存泄漏。

写在最后

JS 的代码,闭包概念随处可见,在使用时也需特别小心,不放心的时候,就将变量释放 xx = null

Web前端入门第 66 问:JavaScript 作用域应用场景(闭包)的更多相关文章

  1. web前端入坑第五篇:秒懂Vuejs、Angular、React原理和前端发展历史

    秒懂Vuejs.Angular.React原理和前端发展历史 2017-04-07 小北哥哥 前端你别闹 今天来说说 "前端发展历史和框架" 「前端程序发展的历史」 「 不学自知, ...

  2. web前端入坑第二篇:web前端到底怎么学?干货资料! 【转】

    http://blog.csdn.net/xllily_11/article/details/52145172 版权声明:本文为博主[小北]原创文章,如要转载请评论回复.个人前端公众号:前端你别闹,J ...

  3. web前端(13)—— 了解JavaScript,JavaScript的引入方式

    从本篇博文开始,将进入web前端方便最关键最重要的部分——javascript,学到后面你就知道它真的太重要了 什么是JavaScript JavaScript一种直译式的脚本语言,是一种动态类型.弱 ...

  4. WEB前端工程师整理的原生JavaScript经典百例

    一.原生JavaScript实现字符串长度截取 二.原生JavaScript获取域名主机 三.原生JavaScript转义html标签 四.原生JavaScript时间日期格式替换 Date.prot ...

  5. Web前端基础怎么学? JavaScript、html、css知识架构图

    以前开发者只要掌握 HTML.CSS.JavaScript 三驾马车就能胜任一份前端的工作了.而现在除了普通的编码以外,还要考虑如何性能优化,如何跨端.跨平台实现功能,尤其是 AI.5G 技术的来临, ...

  6. web前端学习之HTML CSS/javascript之一

    前端编码之路之坎坷,web前端应该一直是个战场吧,各种浏览器的不兼容,各种小细节的修改,要往一个好的产品经理方向走,实在是难,昨天听了一位十年经验的产品经理讲座,最重要的恐怕就是协调资源的能力,而协调 ...

  7. Android零基础入门第66节:RecyclerView点击事件处理

    前面两期学习了RecyclerView的简单使用,并为其item添加了分割线.在实际运用中,无论是List还是Grid效果,基本都会伴随着一些点击操作,那么本期就来一起学习RecyclerView的点 ...

  8. JavaScript作用域链与闭包的理解

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域 链的工作原理. 1. 全局作用域(Global Scope) (1)最外层函数和 ...

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

    闭包引入的前提个人理解是为从外部读取局部变量,正常情况下,这是办不到的.简单的闭包举例如下: function f1(){ n=100; function f2(){ alert(n); } retu ...

  10. Web前端开发规范【HTML/JavaScript/CSS】

    前言 这是一份旨在增强团队的开发协作,提高代码质量和打造开发基石的编码风格规范,其中包含了 HTML, JavaScript 和 CSS/SCSS 这几个部分.我们知道,当一个团队开始指定并实行编码规 ...

随机推荐

  1. FastAPI测试策略:参数解析单元测试

    扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长 探索数千个预构建的 AI 应用,开启你的下一个伟大创意 第一章:核心测试方法论 1.1 三层测试体系架构 # 第一层:模型级测试 def ...

  2. 如何不购买域名在云服务器上搭建HTTPS服务

    step 1: 事前准备 step 1.1: 云服务器 购买一台云服务器(带有弹性公网IP),阿里云,腾讯云,华为云什么的都可以. 选择ubuntu系统 开放安全组策略(把你需要的协议/端口暴露出来) ...

  3. 【Linux】速查手册

    查看Linux系统信息 arch #显示机器的处理器架构(1) uname -m #显示机器的处理器架构(2) uname -r #显示正在使用的内核版本 dmidecode -q #显示硬件系统部件 ...

  4. 开源姿势识别 Demo

    最近项目中要用到姿势识别,调研了 BlazePose. MoveNet 等模型,以下是一些详细的对比. 包括 mediapipe 和 tfjs 等运行环境,webgl,webgpu,wasm 都做了尝 ...

  5. 【Python】Python实现解压rar文件

    Python实现解压rar文件 零.需求 最近在开发一个填分数的应用,需要用到selenium,那么自然需要用到浏览器,浏览器内置到应用中,但是上传到GitCode的时候被限制了,单个文件大小只能是1 ...

  6. Linux下时区/系统时间/硬件时间的设置

    涨姿势,顺带笔记,留爪. 先简述下时区/系统时间/硬件时间的3个主要命令吧 tzselect tzselect命令主要针对时区设置和查看 tz=timezone的缩写,直译=时区 date date命 ...

  7. 阿里巴巴暑期实习 Java 面经,灵犀互娱一面

    哈希表熟悉吗,可以如何实现? 开散列版本什么时候需要扩容 高并发服务器内的主从reactor模型是如何实现的? 进程 线程 协程 的区别? 如何保证线程安全 ? 了解读写锁吗? 单例模式有了解吗? 可 ...

  8. zk源码—3.单机和集群通信原理

    大纲 1.单机版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 2.集群版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 (3)Leader选举阶段 (4)Leader和Fol ...

  9. git版本管理库运用

    一.git 删除本地创建的仓库连接 //删除文件夹下的所有 .git 文件 find . -name ".git" | xargs rm -Rf 二.git命令:全局设置用户名邮箱 ...

  10. 🎀FreeMarker 禁止自动转义标签-noautoesc

    简介 FreeMarker 是一个用 Java 语言编写的模板引擎,它被设计用来生成文本输出(HTML 网页.电子邮件.配置文件等).在 FreeMarker 中,默认情况下,当你在模板中输出变量时, ...