JavaScript 被列为 ‘动态’ 或 ‘解释执行’ 语言,于其余传统语言(如 java)不一样的是,JavaScript是边编译边执行的。
一段源码在执行前会经历三个步骤: 分词/词法分析
-> 解析/语法分析
-> 代码生成
java
这个过程将字符串分解成词法单元,如 var a = 2; 会被分解成词法单元 var、 a、 = 、二、;。空格通常没意义会被忽略git
这个过程会将词法单元转换成 抽象语法树
(Abstract Syntax Tree,AST)。
如 var a = 2; 对应的 抽象语法树
以下, 可经过 在线可视化AST 网址在线分析es6
{ "type": "Program", "start": 0, "end": 10, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 10, "declarations": [ { "type": "VariableDeclarator", "start": 4, "end": 9, "id": { "type": "Identifier", "start": 4, "end": 5, "name": "a" }, "init": { "type": "Literal", "start": 8, "end": 9, "value": 2, "raw": "2" } } ], "kind": "var" } ], "sourceType": "module" }
将 AST 转换成可执行的代码,存放于内存中,并分配内存和转化为一些机器指令github
其实结合上面提到的编译原理,做用域就好理解了。做用域就是当前执行代码对这些标识符的访问权限。
编译器会在当前做用域中声明一些变量,运行时引擎会去做用域中查找这些变量(其实就是一个寻址的过程),若是找到这些变量就能够操做变量,找不到就往上一层做用域找(做用域链的概念),或者返回 null面试
每声明一个函数都会造成一个做用域,那做用域有什么用呢,它能让该做用域内的变量和函数不被外界访问到,也能够反过来讲是不让该做用域内的变量或函数污染全局。数组
对比:网络
var a = 123 function bar() { //... }
和闭包
function foo() { var a = 123 function bar() { //... } }
变量 a 和函数 bar 用一个函数 foo 包裹起来,函数 foo 会造成一个做用域,变量 a 和函数 bar 外界将没法访问,同时变量或函数也不会污染全局。模块化
进一步思考,上面例子的变量 a 和函数 bar 有了做用域,但函数 foo 不也是暴露在全局,也对全局形成污染了啊。是的,JavaScript对这种状况提出了解决方案: 当即执行函数 (IIFE)
函数
(function foo() { var a = 123 function bar() { //... } })()
第一个()将函数变成表达式,第二个()执行了这个函数,最终函数 foo 也造成了本身的做用域,不会污染到全局,同时也不被全局访问的到。
es6以前JavaScript是没有块做用域这个概念的,这与通常的语言(如Java ,C)很大不一样,看下面这个例子:
for (var i = 0; i < 10; i++) { console.log('i=', i); } console.log('输出', i); // 输出 10
for 循环定义了变量 i,一般咱们只想这个变量 i 在循环内使用,但忽略了 i 实际上是做用在外部做用域(函数或全局)的。因此循环事后也能正常打印出 i ,由于没有块的概念。
甚至连 try/catch 也没造成块做用域:
try { for (var i = 0; i < 10; i++) { console.log('i=', i); } } catch (error) {} console.log('输出', i); // 输出 10
解决方法1
造成块做用域的方法固然是使用 es6 的 let 和 const 了, let 为其声明的变量隐式的劫持了所在的块做用域。
for (let i = 0; i < 10; i++) { console.log('i=', i); } console.log('输出', i); // ReferenceError: i is not defined
将上面例子的 var 换成 let 最后输出就报错了 ReferenceError: i is not defined ,说明被 let 声明的 i 只做用在了 for 这个块中。
除了 let 会让 for、if、try/catch 等造成块,JavaScript 的 {}
也能造成块
{ let name = '曾田生' } console.log(name); //ReferenceError: name is not defined
解决方法2
早在没 es6 的 let 声明以前,经常使用的作法是利用 函数也能造成做用域
这么个概念来解决一些问题的。
看个例子
function foo() { var result = [] for (var i = 0; i < 10; i++) { result[i] = function () { return i } } console.log(i)// i 做用在整个函数,for 执行完此时 i 已经等于 10 了 return result } var result = foo() console.log(result[0]()); // 输出 10 指望 0 console.log(result[1]()); // 输出 10 指望 1 console.log(result[2]()); // 输出 10 指望 2
这个例子出现的问题是执行数组函数最终都输出了 10, 由于 i 做用在整个函数,for 执行完此时 i 已经等于 10 了, 因此当后续执行函数 result[x]()
内部返回的 i 已是 10 了。
利用函数的做用域来解决
function foo() { var result = [] for (var i = 0; i < 10; i++) { result[i] = function (num) { return function () { // 函数造成一个做用域,内部变量被私有化了 return num } }(i) } return result } var result = foo() console.log(result[0]()); // 0 console.log(result[1]()); // 1 console.log(result[2]()); // 2
上面的例子也是挺典型的,通常面试题比较考基础的话就会被问道,上面例子不只考察到了块做用域的概念,函数做用域的概念,还考察到了闭包的概念(闭包后续讲但不影响这个例子的理解),多琢磨一下就理解了。
提高指的是变量提高和函数提高,为何JavaScript会有提高这个概念呢,其实也很好理解,由于JavaScript代码是先 编译
后 执行
的,因此在编译阶段就会先对变量和函数作声明,在执行阶段就出现了所谓的变量提高和函数提高了。
console.log(a); // undefined var a = 1;
上面代码 console.log(a); // undefined
就是由于编译阶段先对变量作了声明,先声明了个变量 a, 并默认赋值 undefined
var a; console.log(a); // undefined a = 1;
函数一样也存在提高,这就是为何函数能先调用后声明了
foo(); function foo() { console.log('---foo----'); }
注意:函数表达式不会被提高
foo(); var foo = function() { console.log('---foo----'); } // TypeError: foo is not a function
注意:函数会首先被提高,而后才是变量
var foo = 1; foo(); function foo() { console.log('---foo----'); } // TypeError: foo is not a function
分析一下,由于上面例子编译后是这样的
var foo = undefined; // 变量名赋值 undefined function foo() { // 函数先提高 console.log('---foo----'); } foo = 1; // 但接下去是变量被从新赋值了 1,是个Number类型 foo(); // Number类型固然不能用函数方式调用,就报错了 // TypeError: foo is not a function
闭包问题一直会在JavaScript被提起,是JavaScript一个比较奇葩的概念
闭包的概念: 当函数能够记住并访问所在的词法做用域时,就产生了闭包
概念貌似挺简单的,简单分析下,首先闭包是 产生的
,是在代码执行中产生的,有的一些网络博文直接将闭包定义为 某一个特殊函数
是错的。
闭包是怎么产生的呢,一个函数能访问到所在函数做用域就产生了闭包,注意到做用域的概念,我们最上面的章节有提到,看下面例子:
function foo() { var a = 0; function bar() { a++; console.log(a); } return bar; } var bat = foo() bat() // 1 bat() // 2 bat() // 3
结合例子分析一下: 函数 foo 内部返回了函数 bar ,外部声明个变量 bat 拿到 foo 返回的函数 bar ,执行 bat() 发现能正常输出 1 ,注意前面章节提到的做用域,变量 a 是在函数 foo 内部的一个私有变量,不能被外界访问的,但外部函数 bat 却能访问的到私有变量 a,这说明了 外部函数 bat 持有函数 foo 的做用域
,也就产生了闭包。
闭包的造成有什么用呢,JavaScript 让闭包的存在明显有它的做用,其中一个做用是为了模块化,固然你也能够利用外部函数持有另外一个函数做用域的闭包特性去作更多的事情,但这边就暂且讨论模块化这个做用。
函数有什么做用呢,私有化变量或方法呀,那函数内的变量和方法被私有化了函数怎么和外部作 交流
呢, 暴露出一些变量或方法呀
function foo() { var _a = 0; var b = 0; function _add() { b = _a + 10 } function bar() { _add() } function getB() { return b } return { bar: bar, getB: getB } } var bat = foo() bat.bar() bat.getB() // 10
上面例子函数 foo 能够理解为一个模块,内部声明了一些私有变量和方法,也对外界暴露了一些方法,只是在执行的过程当中顺带产生了一个闭包
上面提到了闭包的产生和做用,貌似在使用 es6语法 开发的过程当中不多用到了闭包,但实际上咱们一直在用闭包的概念的。
foo.js
var _a = 0; var b = 0; function _add() { b = _a + 10 } function bar() { _add() } function getB() { return b } export default { bar: bar, getB: getB }
bat.js
import bat from 'foo' bat.bar() bat.getB() // 10
上面例子是 es6 模块的写法,是否是惊奇的发现变量 bat 能够记住并访问模块 foo 的做用域,这符合了闭包的概念。
本章节咱们深刻理解了JavaScript的 做用域
,提高
,闭包
等概念,但愿你能有所收获,下一部分整理下 this解析
、对象
、原型
等一些概念。
若是有兴趣也能够去个人 github-blog 提 issues
,github也整理了几篇文章会按期更新,欢迎 star