“Give me a chance to know you. ”

更多内容: 移步这里

1. 作用域

1.1. 编译原理

尽管通常将 JavaScript 归类为“动态” 或“解释执行” 语言, 但事实上它是一门编译语言。

程序中的一段源代码在执行之前会经历三个步骤, 统称为“编译”

  1. 分词/词法分析(Tokenizing/Lexing)

    • 这个过程会将由字符组成的字符串分解成(对编程语言来说) 有意义的代码块, 这些代

      码块被称为词法单元(token)。 例如, 考虑程序 var a = 2;。 这段程序通常会被分解成

      为下面这些词法单元: var、 a、 =、 2 、 ;。
  2. 解析/语法分析(Parsing)

    • 这个过程是将词法单元流(数组) 转换成一个由元素逐级嵌套所组成的代表了程序语法

      结构的树。 这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)。

  3. 代码生成

    • 将 AST 转换为可执行代码的过程称被称为代码生成。

1.2. 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时, 就发生了作用域的嵌套。 因此, 在当前作用

域中无法找到某个变量时, 引擎就会在外层嵌套的作用域中继续查找, 直到找到该变量,

或抵达最外层的作用域(也就是全局作用域) 为止。

将作用域处理的过程可视化,如下面的建筑:

作用域是一套规则, 用于确定在何处以及如何查找变量(标识符)。

2. 词法作用域

作用域共有两种主要的工作模型:

  • 词法作用域(重点讨论)
  • 动态作用域(如bash脚本,perl中的一些模式)

2.1. 词法阶段

词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义——名称来历

词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的

如:

function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
} foo( 2 ); // 2, 4, 12

可以将以上代码想象成几个逐级包含的气泡

① 包含着整个全局作用域, 其中只有一个标识符: foo。

② 包含着 foo 所创建的作用域, 其中有三个标识符: a、 bar 和 b。

③ 包含着 bar 所创建的作用域, 其中只有一个标识符: c。

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

查找

作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,作用域查找会在找到第一个匹配的标识符时停止

全局变量会自动成为全局对象(比如浏览器中的window对象)的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。

window.a通过这种技术可以访问那些被同名变量所遮蔽的全局变量。 但非全局的变量

如果被遮蔽了, 无论如何都无法被访问到。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

词法作用域查找只会查找一级标识符,比如a、b和c。如果代码中引用了foo.bar.baz,词法作用域查找只会试图查找 foo 标识符,找到这个变量后, 对象属性访问规则会分别接管对 bar 和 baz 属性的访问

2.2. 欺骗词法

JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..)和with。前者可以对一段包

含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在

运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作

用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

3. 函数作用域和块作用域

究竟是什么生成了一个新的气泡?只有函数会生成新的气泡吗?JavaScript中的其他结构能生成作用域气泡吗?

3.1. 隐藏内部实现

3.1.1. 最小授权|最小暴露原则

指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。——可延伸到如何选择作用域来包含变量和函数

如:

function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3);
} function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2); // 15 /*在这个代码片段中, 变量 b 和函数 doSomethingElse(..) 应该是 doSomething(..) 内部具体实现的“私有” 内容。 给予外部作用域对 b 和 doSomethingElse(..) 的“访问权限” 不仅没有必要且危险*/
// 更合理
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
d
oSomething(2); // 15
//设计上将具体内容私有化
3.1.2. 规避冲突
  1. 全局命名空间

    用变量作为库的命名空间

    所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中

    如:

var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...26
}
};

3.2. 函数作用域

区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

1. 匿名和具名

始终给函数表达式命名是一个最佳实践

setTimeout( function timeoutHandler() { // 快看, 我有名字了
console.log( "I waited 1 second!" );
}, 1000 );

2. 立即执行函数表达式

/*第一种*/
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2 /* 第二种形式*/
(function foo(){ .. })() /*进阶*/
/*将 window 对象的引用传递进去, 但将参数命名为 global*/
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window ); console.log( a ); // 2 /*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
});

函数表达式 def 定义在片段的第二部分, 然后当作参数(这个参数也叫作 def) 被传递进

IIFE 函数定义的第一部分中。 最后, 参数 def(也就是传递进去的函数) 被调用, 并将

window 传入当作 global 参数的值。

函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { .. } 内部)。

4. 变量提升

先有蛋(声明) 后有鸡(赋值)。

JavaScript 引擎将 var a和 a = 2 当作两个单独的声明, 第一个是编译阶段的任务, 而第二个则是执行阶段的任务。无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数) 都会被“移动” 到各自作用域的最顶端, 这个过程被称为提升

只有声明本身会被提升, 而赋值或其他运行逻辑会留在原地。

4.1. 函数优先

函数声明和变量声明都会被提升。 但是一个值得注意的细节(这个细节可以出现在有多个

“重复” 声明的代码中) 是 函数会首先被提升, 然后才是变量。

5. 作用域闭包

闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境

5.1 什么是闭包

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

function foo() {
var a = 2; function bar() {
console.log(a); // 2
}
bar();
}
foo();
//基于词法作用域的查找规则, 函数bar() 可以访问外部作用域中的变量 a
function foo() {
var a = 2; function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友, 这就是闭包的效果。

bar() 显然可以被正常执行。 但是在这个例子中, 它在自己定义的词法作用域以外的地方执行

foo() 执行后垃圾回收器用来释放不再使用的内存空间,闭包的“神奇”之处正是可以阻止这件事情的发生。 事实上内部作用域依然存在,bar() 依然持有对该作用域的引用, 而 这个引用就叫作闭包。

常见的闭包:

function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
//timer 具有涵盖 wait(..) 作用域的闭包, 因此还保有对变量 message 的引用。
//wait(..) 执行 1000 毫秒后, 它的内部作用域并不会消失, timer 函数依然保有 wait(..)作用域的闭包。

只要使用了回调函数, 实际上就是在使用闭包!

5.2. 循环和闭包

for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i); //6
}, i * 1000);
} /*所有的回调函数依然是在循环结束后才会被执行, 因此会每次输出一个 6 出来。*/

原因

缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获” 一个 i 的副本。

但是根据作用域的工作原理, 实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,

但是它们都被封闭在一个共享的全局作用域中, 因此实际上只有一个 i。

我们需要更多的闭包作用域, 特别是在循环的过程中每个迭代都需要一个闭包作用域

//它需要有自己的变量, 用来在每个迭代中储存 i 的值:
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})();
} //使用let
//本质上这是将一个块转换成一个可以被关闭的作用域。
for (var i = 1; i <= 5; i++) {
let j = i; // 是的, 闭包的块作用域!
setTimeout(function timer() {
console.log(j);
}, j * 1000);
} //块作用域和闭包联手
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

5.3. 模块

5.3.1. 模块方式演进

模块有两个主要特征:

  1. 为创建内部作用域而调用了一个包装函数;
  2. 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
//exa1:
//两个私有数据变量 something和 another, 以及 doSomething() 和 doAnother()
//它们的词法作用域(而这就是闭包) 也就是 foo() 的内部作用域。
function foo() {
var something = "cool";
var another = [1, 2, 3]; function doSomething() {
console.log(something);
} function doAnother() {
console.log(another.join(" ! "));
}
} //模块
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
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
//1. CoolModule() 只是一个函数, 必须要通过调用它来创建一个模块实例。 如果不执行外部函数, 内部作用域和闭包都无法被创建。
//2. CoolModule() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。 这个返回的对象中含有对内部函数而不是内部数据变量的引用 //改进
var foo = (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
};
})();
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
};
return publicAPI;
})("foo module");
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创造了可以观察和实践闭包的条件。

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

5.3.2. ES6的模块

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()
);
}

import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上(在我们的例子里是hello)。module会将整个模块的API导入并绑定到一个变量上(在我们的例子里是foo)。export会将当前模块的一个标识符(变量、函数)导出为公共API。这些操作可以在模块定义中根据需要使用任意多次。

5.3.3. 动态作用域

动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。

function foo() {
console.log(a); //2 —— 如果是动态作用域3
} function bar() {
var a = 3;
foo();
}
var a = 2;
bar();

JavaScript并不具有动态作用域。它只有词法作用域

主要区别:

词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用

6. this词法

6.1. _self

常见this绑定丢失解决方案: 'var _self = this'

6.2. 箭头函数

ES6 中的箭头函数引入了一个叫作 this 词法的行为

var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(() => { // 箭头函数是什么鬼东西?
this.count++;
console.log("awesome?");
}, 100);
}
}
};
obj.cool(); // 很酷吧 ?

简单来说,箭头函数在涉及this绑定时的行为和普通函数的行为完全不一致。它放弃了所有普通this绑定的规则,取而代之的是用当前的词法作用域覆盖了this本来的值

这个代码片段中的箭头函数只是“继承”了cool()函数的this绑定(因此调用它并不会出错)。

6.3. bind

//正确使用和包含 this 机制
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(function timer() {
this.count++; // this 是安全的
// 因为 bind(..)
console.log("more awesome");
}.bind(this), 100); // look, bind()!
}
}
};
obj.cool(); // 更酷了。

You don't know js的更多相关文章

  1. Vue.js 和 MVVM 小细节

    MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自 ...

  2. js学习笔记:操作iframe

    iframe可以说是比较老得话题了,而且网上也基本上在说少用iframe,其原因大致为:堵塞页面加载.安全问题.兼容性问题.搜索引擎抓取不到等等,不过相对于这些缺点,iframe的优点更牛,跨域请求. ...

  3. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  4. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  5. jquery和Js的区别和基础操作

    jqery的语法和js的语法一样,算是把js升级了一下,这两种语法可以一起使用,只不过是用jqery更加方便 一个页面想要使用jqery的话,先要引入一下jqery包,jqery包从网上下一个就可以, ...

  6. 利用snowfall.jquery.js实现爱心满屏飞

    小颖在上一篇一步一步教你用CSS画爱心中已经分享一种画爱心的方法,这次再分享一种方法用css画爱心,并利用snowfall.jquery.js实现爱心满屏飞的效果. 第一步: 利用伪元素before和 ...

  7. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  8. JS正则表达式常用总结

    正则表达式的创建 JS正则表达式的创建有两种方式: new RegExp() 和 直接字面量. //使用RegExp对象创建 var regObj = new RegExp("(^\\s+) ...

  9. 干货分享:让你分分钟学会 JS 闭包

    闭包,是 Javascript 比较重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,很难从定义去理解它.因此,本文不会对闭包的概念进行大篇幅描述 ...

  10. JS核心系列:理解 new 的运行机制

    和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象. 但在 javascript 中,万物皆对象,为什么还要通过 ...

随机推荐

  1. JavaScript+svg绘制的一个动态时钟

    结果图: 代码如下: <!DOCTYPE html> <html> <head> <title>动态时钟</title> </head ...

  2. Codeforces Round #424 (Div. 2, rated, based on VK Cup Finals)A B题

    当时晚上打CF时候比较晚,加上是集训期间的室友都没有晚上刷题的习惯,感觉这场CF很不在状态.A题写复杂WA了一发后去厕所洗了个脸冷静了下,换个简单写法,可是用cin加了ios::sync_with_s ...

  3. 猜年龄---while循环

    #!/usr/bin/env python# -*- coding:utf-8 -*-# Author:Andy Chen age_of_oldboy = 56 count = 0while True ...

  4. 用 volume container 共享数据 - 每天5分钟玩转 Docker 容器技术(42)

    volume container 是专门为其他容器提供 volume 的容器.它提供的卷可以是 bind mount,也可以是 docker managed volume.下面我们创建一个 volum ...

  5. Win7 JBOSS的下载安装、环境变量配置以及部署

    1. 下载安装 http://jbossas.jboss.org/downloads/ 我下载的是:JBoss AS7.1.1.Final 2. 解压安装包  D:\Java\jboss-as-7.1 ...

  6. 基于Spring4的定时任务管理

    在项目中,有时会遇到定时任务的处理,下面介绍一下我的做法. 此做法基于Spring4,Spring框架搭建成功,另需引入quartz.jar,pom.xml文件中加入 <dependency&g ...

  7. Golang 基于libpcap/winpcap的底层网络编程——gopacket安装

    Go简介 Go是一种编译型语言,它结合了解释型语言的游刃有余,动态类型语言的开发效率,以及静态类型的安全性. 语法类似C/C++,但是又带有一点python的味道 其中个人认为最出色的特点就是他的包管 ...

  8. Spring源码情操陶冶-AbstractApplicationContext#prepareRefresh

    前言-阅读源码有利于陶冶情操,本文承接前文Spring源码情操陶冶-AbstractApplicationContext 约束: 本文指定contextClass为默认的XmlWebApplicati ...

  9. 跨主机网络概述 - 每天5分钟玩转 Docker 容器技术(48)

    前面已经学习了 Docker 的几种网络方案:none.host.bridge 和 joined 容器,它们解决了单个 Docker Host 内容器通信的问题.本章的重点则是讨论跨主机容器间通信的方 ...

  10. es6 解构赋值

    ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 关于给变量赋值,传统的变量赋值是这样的: var arr = [1,2,3];//把数组的值 ...