《你不知道的JavaScript》第一部分:作用域和闭包

时间:2023-02-23 13:10:04

第1章 作用域是什么

抛出问题:程序中的变量存储在哪里?程序需要时,如何找到它们?

设计 作用域目的:为了更好地存储和访问变量。

作用域:根据名称查找变量的一套规则,用于确定在何处以及如何查找变量(标识符)。

☞ 编译原理

JavaScript 是一门编译语言,但它不是 提前编译的,编译结果也不能在分布式系统中进行移植。

程序的源代码在执行前的三个步骤,统称为“编译”:

  1. 分词/词法分析:将字符串分解成代码块(词法单元)
  2. 解析/语法分析:将词法单元流(数组)转换成抽象语法树(Abstract Syntax Tree, AST)
  3. 代码生成:将AST转换成可执行代码

JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化。JavaScript 的编译不是发生在构建之前,而是发生在代码执行前的几微秒,甚至更短。因此,JavaScript 引擎用尽了各种方法来保证新更能最佳。

☞ 理解作用域

  • 引擎

    从头到尾负责整个JavaScript程序的编译及执行过程;

  • 编译器

    负责语法分析及代码生成;

  • 作用域

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

总结:变量的赋值操作会执行两个动作——>

1)编译器会在当前作用域中声明一个变量(如果之前没有声明过);

2)在运行时,引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

引擎的LHS查询和RHS查询

  • LHS查询:赋值操作的左侧,试图找到变量的容器本身,从而可以对其赋值;

      // 不关心当前的值是什么,只是想要为 =2 这个赋值操作找到一个目标
    a = 2;

不成功的 LHS引用 会导致自动隐式创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常。

  • RHS查询:赋值操作的右侧,查找某个变量的值。

      // a 并没有赋予任何值
    // 需要查找并取得 a 的值,这样才能传递给 console.log(...)
    console.log( a );

不成功的 RHS 查询引用会抛出 ReferenceError 异常。

嵌套作用域

LHS 查询和 RHS 查询都会在当前执行作用域中开始,如果没有找到,就会向上级作用域继续查找目标标识符,直至抵达全局作用域,便停止。

第2章 词法作用域

作用域的两种工作模型:

  • 词法作用域:定义在词法阶段的作用域,即,是由你在写代码时将变量和块作用域写在哪里来决定的。
  • 动态作用域:在运行时确定,关注函数从何处调用——>this。

《你不知道的JavaScript》第一部分:作用域和闭包

作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。

☞ 查找

作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。

☞ 欺骗词法

—— 在运行期修改书写期的词法作用域,会导致性能下降,不要使用。

(1) eval: 接受一个字符串作为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。会修改词法作用域。

function foo(str, a) {
eval( str ); // 欺骗!
console.log(a, b);
} var b = 2; foo( 'var b = 3;', 1); // 1,3

在严格模式中,无法修改所在的作用域。

(1) with: 重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。会重新创建一个全新的词法作用域。

在严格模式中,with 被完全禁止。

第3章 函数作用域和块作用域

☞ 函数作用域

函数作用域:指属于这个函数的全部变量都可以在整个函数的范围内使用及复用(嵌套作用域也可以)。

  • 最小特权原则(最小授权/最小暴露原则)

    指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。

隐藏作用域的好处:

  • 遵循最小特权原则,避免暴露过多的变量和函数;
  • 避免同名标识符之间的冲突。

规避冲突的方式:1)全局命名空间;2)模块管理。

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。前者会绑定在所在作用域中,而后者会绑定在函数表达式自身的函数中而不是所在作用域中。

行内函数表达式 可以解决匿名函数表达式的缺点。始终给函数表达式命名是一个最佳实践。

setTimeout(function timeoutHandler() {
//...
}, 1000);

☞ 立即执行函数表达式

具名函数的IIFE(立即执行函数表达式)具有匿名函数表达式的所有优势,也是值得推广的实践。

  • IIFE 的进阶用法:把它们当作函数调用并传递参数进去。

      var a = 2;
    (function IIFE(global) {
    var a = 3;
    console.log(a); //3
    console.log(global.a); // 2
    })(window); console.log(a); // 2
  • 这个模式的另一个应用场景:解决 `undefined 标识符的默认值被错误覆盖导致的异常(虽然不常见)

      undefined = true;	// 反模式,绝对不要这么做!
    
      (function IIFE(undefined) {
    var a;
    if (a === undefined) {
    console.log('undefined is safe here!');
    }
    })();
  • IIFE 还有一种变化的用途:倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去。

      var a = 2;
    (function IIFE(def) {
    def(window);
    })(function def(global) { var a = 3;
    console.log(a); // 3;
    console.log(global.a); // 2
    });

☞ 块作用域

块作用域的好处:变量的声明应该距离使用的地方越近越好,并最大限度地本地化。

块作用域的例子:with、try/catch、let、const

使用 let 进行的声明不会在块作用域中进行提升。

第4章 提升

引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。

包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。这个过程就好像所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

[例子]
	// 我们习惯将它看作一个声明
var a = 2; /**
* JavaScript 引擎会把它当作两个单独的声明
*/
var a; // 定义声明,在编译阶段进行
a = 2; // 赋值声明,在执行阶段进行
  • 每个作用域都会进行提升 操作。但,函数声明会被提升,函数表达式不会被提升。

  • 避免在块内部声明函数。

第5章 作用域闭包

函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
var a = 2; function bar() {
console.log(a);
} return bar;
} var baz = foo(); // 实际上只是通过不同的标识符引用调用了内部的函数 bar()
baz(); // 2 —— 闭包的效果

由于 bar() 声明的位置,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后的任何时间进行引用。

bar() 依然持有对该作用域的引用,这个引用就叫作闭包

闭包的作用:阻止垃圾回收器回收内部作用域,使得函数在定义时的词法作用域以外的地方被调用时,仍然可以继续访问定义时的词法作用域。

[闭包示例2:]
function foo() {
var a = 2; function baz() {
console.log(a);
} bar(baz);
} function bar(fn) {
fn(); // 闭包
} foo(); // 2
[闭包示例3:]
var fn;

function foo() {
var a = 2; function baz() {
console.log(a);
} fn = baz; // 将 baz 分配给全局变量
} function bar() {
fn(); // 闭包
} foo(); bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

☞ 模块——利用闭包实现

最常见的实现模块模式的方法通常被成为 模块暴露

function CoolModule() {
var something = 'cool';
var another = [1,2,3]; function doSomething() {
console.log(something);
} function doAnother() {
console.log(another.join('!'));
} return {
doSomething : doSomething,
doAnother : doAnother
}
} /**
* 必须通过调用 CoolModule() 来创建一个模块实例
* 如果不执行外部函数,内部作用域和闭包都无法被创建
*/
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3

模块模式需要具备两个条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

一个具有函数属性的对象并不是真正的模块;

一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。

[单例模式:当只需要一个实例时]:
var foo = (function CoolModule() {
// ...
})(); foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
[模块模式的变化用法:命名将要作为公共API返回的对象]:
var foo = (function CoolModule(id) {
function change() {
// 修改公共API
publicAPI.identify = identify2;
} function identify1() {
console.log(id);
} function identify2() {
console.log(id.toUpperCase());
} var publicAPI = {
change: change,
identify: identify1
}
})('foo module'); foo.identify(); // 'foo module'
foo.change();
foo.identify(); // 'FOO MODULE'

通过在模块内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。

☞ 现代的模块机制(没太看明白)

大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的 API。

var MyModules = (function Manager() {
var modules = {}; function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
} function get(name) {
return modules[name];
} return {
define: define,
get: get
};
})(); // 定义模块
MyModules.define('bar', [], function() {
function hello(who) {
return 'Let me introduce: ' + who;
} return {
hello: hello
};
}); MyModules.define('foo', ['bar'], function() {
var hungry = 'hippo'; function awesome() {
console.log(bar.hello(hungry).toUpperCase());
} return {
awesome: awesome
};
}); var bar = MyModules.get('bar');
var foo = MyModules.get('foo'); console.log(
bar.hello('hippo')
); // Let me introduce: hippo foo.awesome();

☞ 未来的模块机制

ES6 的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”可以在导入模块时异步地加载模块文件。

bar.js

function hello(who) {
return "Let me introduce: " + who;
} export hello;

foo.js

// 仅从 'bar' 模块导入 hello()
import hello from 'bar'; var hungry = 'hippo'; function awesome() {
console.log(
hello(hungry).toUpperCase()
);
} export awesome;

baz.js

// 导入完整的 “foo” 和 “bar” 模块
module foo from 'foo';
module bar from 'bar'; console.log(
bar.hello('rhino')
); // Let me introduce: rhino foo.awesome(); // LET ME INTRODUCE: HIPPO
  • import:将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上;
  • module:将整个模块的API导入并绑定到一个变量上;
  • export:将当前模块的一个标识符(变量、函数)导出为公共API。

《你不知道的JavaScript》第一部分:作用域和闭包的更多相关文章

  1. javascript&lpar;面向对象,作用域,闭包,设计模式等&rpar;

    javascript(面向对象,作用域,闭包,设计模式等) 1. 常用js类定义的方法有哪些? 参考答案:主要有构造函数原型和对象创建两种方法.原型法是通用老方法,对象创建是ES5推荐使用的方法.目前 ...

  2. JavaScript中的作用域和闭包

    首先强烈安利<你不知道的JavaScript>,JS初学者进阶必读. 对于从C++.Java等静态语言转向JavaScript的初学者(比如我)来说,JS一些与众不同而又十分要紧的特性使得 ...

  3. javascript 函数和作用域(闭包、作用域)(七)

    一.闭包 JavaScript中允许嵌套函数,允许函数用作数据(可以把函数赋值给变量,存储在对象属性中,存储在数组元素中),并且使用词法作用域,这些因素相互交互,创造了惊人的,强大的闭包效果.[upd ...

  4. 你不知道的js异步、作用域、闭包

    例题如下: for (var i = 0; i < 3; i++) {     setTimeout(function() {         console.log(i);     }, 0) ...

  5. 你不知道的JavaScript——第一章:作用域是什么?

    编译原理 JavaScript事实上是一门编译语言,但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植. 任何JavaScript代码片段在执行前都要进行编译(通常就在执行 ...

  6. JavaScript概念总结&colon;作用域、闭包、对象与原型链

    1 JavaScript变量作用域 1.1 函数作用域 没有块作用域:即作用域不是以{}包围的,其作用域完成由函数来决定,因而if /for等语句中的花括号不是独立的作用域. 如前述,JS的在函数中定 ...

  7. Javascript深入之作用域与闭包

    相信绝大多数同学都听过闭包这个概念,但闭包具体是什么估计很少有人能够说的很详细.说实话闭包在我们平时开发中应该是很常见的,并且在前端面试中闭包也是常见的重要考点,在学习闭包之前我们先来看看作用域与作用 ...

  8. Javascript中关于作用域和闭包和域解释的面试题

    <script type="text/javascript"> function fn() { var i = 10; return function (n) { co ...

  9. 你不知道的JavaScript(上)作用域与闭包

    第一部分 作用域与闭包 第一章 作用域是什么 1.作用域 变量赋值操作会执行两个动作:首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后会在运行时引擎会在作用域中查找该变量,找到就会 ...

  10. javascript作用域和闭包之我见

    javascript作用域和闭包之我见 看了<你不知道的JavaScript(上卷)>的第一部分--作用域和闭包,感受颇深,遂写一篇读书笔记加深印象.路过的大牛欢迎指点,对这方面不懂的同学 ...

随机推荐

  1. 【luogu】 P1880 石子合并

    原题原题原题原题原题 先贴上错误代码... ↓错误代码↓ #include <iostream> #include <cstdio> #include <cstring& ...

  2. org&period;springframework&period;dao&period;DataIntegrityViolationException&colon;

    数据库用的hibernate,开发工具用的myeclipse,使用开发工具连接数据库生成hibernate基于xml的po类,运行时报org.springframework.dao.DataInteg ...

  3. 动态添加Marquee标签,并动态赋值与属性

    前台加载js $(function(){ var publishStr="<%=publishText%>" var marqueeStr=" <mar ...

  4. Cpdetector编码识别

    概述 浏览器在打开一个网页时,首要任务是判断网页的编码格式,然后采用合适的编码进行解析:我们常用的文本编辑器在打开文档时同样需要判断文档的编码进行相应的解析.这涉及到的技术就是编码甄别,下面我们介绍一 ...

  5. c&num; 调用 友盟api

    今天要使用友盟的推送API来给我的app进行推送信息,调试了好久,老是返回500错误,最终在友盟的技术人员支持下完成了此操作,在此多谢友盟技术和客服人员. 把发方法和注意事项贴出来供大家参考. pub ...

  6. 前端css要加的一些

    编码格式 @charset "utf-8"; body的外边距设置 margin:0; 标签设置 form,ul,ol,li设置为padding:0; ul,ol,li设置为lis ...

  7. Android版本28使用http请求报错not permitted by network security policy

    Android版本28使用http请求报错not permitted by network security policy android模拟器调试登录的时候报错 CLEARTEXT communic ...

  8. TCP&sol;IP协议---ICMP协议及ping、traceroute

    ICMP Internet控制报文协议通常被认为是IP层的组成部分,一般被IP层或更高层(TCP.UDP)使用.ICMP报文是在IP数据报内部被传输的.如图: ICMP报文的格式如下: 报文的前4个字 ...

  9. 读jQuery源码有感2

    那么就来读读jQuery源码的Callbacks部分. 一上来看原版源码 jQuery.Callbacks = function( options ) { // Convert options fro ...

  10. js 记录

    (function(win,doc) { })(window,document)