与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植
JavaScript 是先编译,编译完就会立即执行。JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
作用域是根据名称查找变量的一套规则。
处理过程分为两个部分,编译和执行
编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。然后编译器第二步就会生成代码,接着引擎
开始执行它,执行过程会进行查询。有两种查询方式
简单概况:如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;
如果目的是获取变量的值,就会使用 RHS 查询。
赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
在查询过程中,LHS 和 RHS 查询都会在当前执行作用域中开始,没找到就会一直向上级作用域查找,最后达到全局。
引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,
全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。
比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,
那么引擎会抛出另外一种类型的异常,叫作 TypeError。
测验答案(自己做的)
lsh:1. 引擎找c var c 2. 引擎找a 函数传递形参时,a作为形参 3. 引擎找b rhs: 1. foo函数rhs引用 2.a的rhs引用 将a赋予b 3. b的rhs引用 a+b 4..a的rhs引用 a+b
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,
因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)。后面会有欺骗词法(一般不用)
在多层的嵌套作用域中可以定义同名的 标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,
作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见 第一个匹配的标识符为止。
function foo(str, a) { eval( str ); // 欺骗! console.log( a, b ); } var b = 2; foo( "var b = 3;", 1 ); // 1, 3```
var obj = { a: 1, b: 2, c: 3 }; // 单调乏味的重复 "obj" obj.a = 2; obj.b = 3; obj.c = 4; // 简单的快捷方式 with (obj) { a = 3; b = 4; c = 5; }
//可以这样理解,当我们传递 o1 给 with 时,with 所声明的作用域是 o1,而这个作用域中含 //有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符, //因此进行了正常的 LHS 标识符查找(查看第 1 章)。 //o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行 //时,自动创建了一个全局变量(因为是非严格模式)。 function foo(obj) { with (obj) { a = 2; } } var o1 = { a: 3 }; var o2 = { b: 3 }; foo( o1 ); console.log( o1.a ); // 2 foo( o2 ); console.log( o2.a ); // undefined console.log( a ); // 2——不好,a 被泄漏到全局作用域上了
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复
用(事实上在嵌套的作用域中也可以使用)
可以使用函数作用域实现隐藏内部实验保护其内部的函数和变量。避免暴露更多的变量和函数,减少对全局的污染。
“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,
两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致
变量的值被意外覆盖
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ... } };
IIFE:立即执行函数表达式;三种形式:
1. (function IIFE(){ })(); 2. (function IIFE(){ }()); 3. (function IIFE( def ) { def( window ); })(function def( global ) { ... });
try { undefined(); // 执行一个非法操作来强制制造一个异常 } catch (err) { console.log( err ); // 能够正常执行! } console.log( err ); // ReferenceError: err not found
a = 2; var a; console.log( a ); //你认为 console.log(..) 声明会输出什么呢?
编译器:先编译(声明等等)在执行(赋值等等执行语句);
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
函数声明会被提升,但是函数表达式却不会被提升。函数会首先被提升,然后才是变量。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。
如下图代码会输出 1 而不是 2
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的
最顶端,这个过程被称为提升
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行
可以理解为闭包是指有权访问另一个函数作用域中的变量的函数。
例子1:
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2 —— 朋友,这就是闭包的效果。
由于baz对foo函数的引用,导致foo函数内部的变量不会被销毁。它拥有涵盖 foo() 内部作用域的闭包,
使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
在上面例子中,bar作为foo函数内部和外部之间的一座桥梁。
有聪明的同学就会不理解如果不是闭包为什么变量会被销毁
因为js每次调用函数时,都会创建一个新的对象来保存局部变量,然后将这个对象传入作用域链,接着在执行阶段就可以访问
在函数调用完返回时,就会将这个对象从作用域链中删除,除非有特殊情况(嵌套的函数等)。
在解释一下,因为子元素也会有一个对象来保存局部变量,但不止局部变量,他父函数中的变量也会被他保存下来。
例子2:
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // 妈妈快看呀,这就是闭包! }
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包
函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。
把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a
例子3:
function wait(message) { setTimeout( function timer() { console.log( message ); }, 1000 ); } wait( "Hello, closure!" )
在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
例子4
//关于IIFE模式 var a = 2; (function IIFE() { console.log( a ); })();
虽然这段代码可以正常工作,但严格来讲它并不是闭包。为什么?因为函数(示例代码中的 IIFE)并不是在它本身的词法作用域以外执行的.
尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包.
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次 6
那要怎么解决呢?
从根源上发现,出现的原因为他们都共享一个变量i,所以我们需要块级作用域将i包裹起来。
刚好IIFE可以起到封闭的作用
for (var i=1; i<=5; i++) { (function() { setTimeout( function timer() { console.log( i ); }, i*1000 ); })(); }
但如上图这个样包裹并没有效果,因为这里还是共享一个i,可以在IIFE括号中、函数上面加一个局部变量j来储存i。
上面那个IIFE例子貌似解决了问题,但很麻烦。这时候块级作用域正好满足解决问题。
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随
后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
将上面的例子变化一下
for (var i=1; i<=5; i++) { let i = i; // 是的,闭包的块作用域! setTimeout( function timer() { console.log( i ); }, i*1000 ); }
这里let创建了一个类似于{}的块级作用域,函数类似于块级作用域。