Open HuangHongRui opened 6 years ago
什么是作用域: a. 用于存储变量, 以便于便捷查找这些变量的规则.
作用域嵌套: a. 当有多层代码块或函数互相嵌套时(函数/代码块存于函数/代码块中),则出现多层作用域,也成为多层嵌套.
寻找变量: a. 当寻找某变量时,会在当前作用域开始寻找,如果找不到目标,引擎则会往上一层作用域继续寻找.直到找到变量 或是 抵达全局作用域为止. b. Example: 当执行该函数时,抵达函数作用域里面时,引擎会对name进行 RSH查询 引用. 在函数作用域无name这个变量,所以引擎会往外层作用域(Global)继续找.找到并Print出来.
function getName() {
console.log(name)
}
let name = 'Rui'
getName()
作用域比喻图: a. 如图.可以把它想象成一栋楼来加深对作用域的印象与理解.. b. 第一层为当前执行的作用域.顶层为最外面的全局作用域. c. LSH & RSH 查询都会在当前层进行查找..如果没找到,往上找.最终无论是到达顶层还是是否找到目标变量,查找过程都将停止.
._______.______________._______.
|_______|___全局作用域___|_______|
|_______|______________|_______|
|___^___|______________|___^___|
|___|___|______________|___|___|
|___|___|______________|___|___|
|_______|______________|_______|
|_______|___当前作用域___|_______|
2种主要工作模型: a. 词法作用域.(普片) b. 动态作用域.(如Bash脚本/Perl等)
词法作用域: a. 是定义在词法阶段的作用域. b. 写代码时将变量和块作用域写在哪所决定的.( 大部分情况下,词法分析器处理代码时,会保持作用域不变 ) c. 全局作用域,只有一个标识符=getLastName, getLastName所创建的作用域包含的标识符=lastName/firstName/getAge, 而getAge创建的作用域也只有一个标识符=age
Example:
.__________________________________________________.
| |
| function getLastName(lastName) { |
| .__________________________________________. |
| | var firstName = 'HongRui' | |
| | | |
| | function getAge(age) { | |
| | .______________________________________. | |
| | | | | |
| | | console.log(age, firstName, lastName)| | |
| | |______________________________________| | |
| | | |
| | } | |
| | | |
| | getAge(18) | |
| |__________________________________________| |
| |
| } |
| getLastName('Huang') |
|__________________________________________________|
大多标准语言编译器的第一个工作阶段: a. 词法化.( 单词化 ) b. 词法化过程: 对源码中的字符进行检查,如果是 [ 有状态 ] 解析过程,那么会赋予单词语义.
查找: a. 作用域之间的结构和互相之间的位置关系,会提供给引擎足够的位置信息,有助于引擎的搜索. b. 词法作用域查找只会查找一级标识符!词法作用域查找只会试图查找 a 标识符.找到该变量后,[ 对象属性管理规则 ]会分别接管对 b 和 c 属性的访问.
b-Example:
a = {
b : {
c : 'Rui'
}
}
console.log (a.b.c);
遮蔽效应:
a. 作用域会查找到第一个匹配的标识符时停止.
b. 在多层作用域中可定义同名标识符.从当前作用域往上查找时,找到目标标识符则停止. (简单理解: 先到先得.)
c. 同名的标识符[ a ]如果位于global全局,则自动成为window
对象的属性.且可以使用window.a
被进行访问..除第一个访问到的同名变量[ a ]与全局变量[ a ]外,其他同名变量[ a ]将无法访问到.
欺骗词法:
a. 两种使用机制: eval(..)
with(..)
b. 导致性能下降.
[ 1 ] eval: 接收一个字符串为参数.参数内容将在于程序中的生成代码.(如本就书写在此)..
(1-a): eval 如果接收了含一或多个声明代码,就会修改起所处的词法作用域.
(1-b): 例子中,只是一个简单的固定代码,实际情况中,可以根据逻辑动态的将字符拼接到一起再传递进去(通常被用来执行动态创建代码).
(1-c): eval 可以在运行期修改书写期的词法作用域.
(1-d): eval 在严格模式下,会有自己的词法作用域.这导致其中的声明无法修改所在的作用域.(简单理解为:该机制废了.)
(1-e): 类似 eval 的方法有: setInterval(str, ..)
, new function(.., str)
Ea.Example
function getName(str) {
eval(str)
console.log(name)
}
getName("var name = 'Rui'")
//相等于:
function getName() {
var name = 'Rui'
console.log(name)
}
getName()
[ 2 ] with: 常被当做重复引用一个对象中的多个属性的快捷方法. (2-a): 可将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域..因此这个对中的属性也会被处理为定义在这个作用域中的标识符. (2-b): with声明实际上根据传递给它的对象凭空创建一个新的词法作用域 (2-c): Example中,with 代码块中为 b 赋值了 22, 这里,先需了解到都会引用LSH查询.当在所有作用域中查不到该值时,会于Global中自动创建一个.所以,最后 window.b === 22 | true (a-d): 严格模式下,会被完全禁止...
proObj = {
a: 1,
}
一般改值:
proObj.a = 11
with改值:
with(proObj) {
a = 11,
b = 22 // 注意点
}
性能问题: a. JavaScript 引擎会在编译阶段进行多项性能优化,有些会依赖 代码的词法进行静态分析.并预先确定所有变量&函数的定义位置.才能在执行过程中快速找到标识符. b. 如果代码中使用了欺骗词法: eval(..) with(..), 引擎发现后,只能简单假设关于标识符位置的判断是无效的!!!(因为无法知道词法分析阶段明确知道eval会接收什么代码且对作用域有什么修改 | 也无法知道传递给with用来创建新新词法作用域的对象内容是什么) c. 如果出现 eval(..), with(..) 那么..所有优化将是多余的..唯一的对策就是不优化..(猥琐笑..) d. 大量使用它们,会导致运行起来非常慢!!!
作用域包含一系列的"气泡/容器": a. 每个"气泡/容器"包含李敖标识符(变量/函数)的定义,它们互相嵌套且整齐排列,排列的结构是在写代码时定义的. b. JavaScript 基于函数的作用域.意味这每声明一个函数都会为其自身创建一个"气泡/容器"
函数作用域的含义: a. 属于这个函数的全部变量都可以在整个函数的范围内使用以及复用(嵌套的作用域中也可以使用).
隐藏内部实现: a. 从所写的代码中挑选出任意一个代码片段, 并用函数声明对它进包装, 这就能把这些代码隐藏起来. b. 简单理解,就是在挑选出来的代码片段周围创建一个作用域气泡, 那这段代码中的任何声明(变量/函数)都会绑定在这个新创建的气泡(包装函数)的作用域中.
最小特权原则(最小授权/最小暴露原则): a. 在软件设计中, 应该最小限度地暴露必要的内容,从而将其他内容"隐藏"起来.(如模块或对象API)
"隐藏" 变量和函数: a. 多钟原因促成了这种基于作用域的隐藏方法. b. 多部分都是从 最小特权原则(最小授权/最小暴露原则) 中引申过来的. c. 如下代码,只需要暴露getName即可,其他均是getName的"私有"内容, 暴露在外面反而危险(当全局变量多的时候).. d. 好处是可避免同名标识符之间的冲突.
//暴露:
function getName(firstName) {
name = info + firstName + getLastName()
console.log( name )
}
function getLastName() {
return " huang "
}
var info = " Hello! "
getName("Rui")
//隐藏:
function getName(firstName) {
var info = " Hello! "
function getLastName() {
return " huang "
}
name = info + firstName + getLastName()
console.log( name )
}
getName("Rui")
如果函数不需要函数名(为了函数名可以不污染所在作用域,并能自动运行): a. 可以使用IIFE(立即执行函数),函数会被当做函数表达式而不是一个标准的函数声明来处理.. b. 如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式.
函数声明和函数表达式之间最主要的区别:
a. 它们的名称标识符会绑定在何处.
b. function rui()
被绑定在所在作用域中,可通过 rui()
来调用.
c. (function rui())
作用域则是绑定在自身函数中(不在所在作用域中),
匿名函数的缺点: a. 在栈追踪中不会显示函数名,导致调试困难. b. 函数无法递归调用自身/事件触发后解绑自身等. c. 忽略代码可读性,一个描述性的名称可让代码不言自明. d. 对应策略: 始终给函数表达式指定一个函数名可解决以上问题(最佳实践).
立即执行函数表达式: a. 全写: Immediately Invoked Function Expression/ 简写: IIFE b. 由一对()包含的函数,因而成为一个表达式.再于尾部加上一对()可以立即执行这个函数. c. 具名/匿名,各有其好处..看各自爱好了.
IIFE 另一种用途: a. 广泛适用于 Universal Module Definition 项目. b. 特点是,倒置代码的运行顺序,冗长,易于理解. c. Example:
var name = 'Rui';
(function tool(argFun) {
argFun("Huang")
})(function(lastName) {
console.log(name + lastName)
})
块作用域:
a. for(..){}
for循环, if(..){}
if语句, with
, try/catch
的catch分句等等.都属于块作用域..
b. 块作用域是一个用于对之前的最小授权原则进行扩展的工具.
现有鸡还是先有蛋?
任何声明在某个作用域中的变量, 都将属于这个作用域
先吃栗子:
(a)
name = 'Rui';
console.log(name) // Print: 'Rui'
var name;
(b)
console.log(name) // Print: 'undefined'
var name = 'Rui';
(c)
getName();
function getName() {
console.log(name) // Print: 'undefined'
var name = 'Rui'
}
编译器回炉
a. 上面栗子需回顾[ 编译器 ]的知识内容.
b. 引擎会在解释JavaScript代码前先对其进行编译.
c. 编译阶段中, 部分工作是找到所有声明.
d. 用合适的作用域将它们关联起来.(词法作用域核心)
e. 思路: 包括变量和函数的所有声明都会在任何代码被执行前, 先一步被处理!
f. 简单的: var name = 'Rui'
为两个声明, 第一个 var name
声明在编译阶段进行, 第二个 name = 'Rui'"
留于原地等待执行阶段.
g. 变量和函数声明从它们在代码中出现的位置被"移动"到最上面,该过程称为"提升".
h. 现有蛋(声明),后有鸡(赋值)
函数表达式的问题.
a. var getName
被提升分配给所在作用域顶部(此时: 值 === undefined).
b. 此时 getName 还没有被赋值. 仍停留原地等待执行阶段.(如果是函数声明则会直接赋值)
c. getName()
对值为 undefined 的getName 进行调用.导致非法操作..因此抛错. (Print: TypeError)
d. 具名的表达式, 名称标识符在赋值之前也无法在所在作用域中使用.
e. 栗子:
(a)
getName(); // Print: "TypeError"
var getName = function() {
console.log('Rui')
}
(b)
getName() // ReferenceError
getXing() // TypeError
var getXing = function getName() {
console.log("Rui")
}
函数优先. a. 函数和变量声明均会被提升. b. 细节: 提升优先级为 > 函数先,变量后 c. 同名变量声明 遇到 同名函数声明, 编译器会检查作用域是否有相识标识符. 有则忽略声明, 继续编译. d. 后面的 同名函数声明 会覆盖 前面的 同名函数声明. e. 栗子:
(a)
Rui() // Print: '1'
var Rui;
function Rui() {...1}
Rui = function() {...2}
(b)
Rui() // Print: '3'
function Rui() {...1}
function Rui() {...2}
function Rui() {...3}
小结: a. 所有声明均会被提升至 作用域 顶端.(该过程成为提升) b. 先有鸡(声明), 后有蛋(赋值). c. 函数优先提升. 同为同名函数则后面的声明覆盖前面的.
编译原理
JavaScript 属于哪款类型语言: a. 表面为 [动态/解释执行] 型语言. b. 实则为 [编译型] 语言.
传统的源代码执行时会经过编译: 过程有三个步骤. (1) 分词/词法分析; a. 将字符/串代码转换词法单元/流(代码块). b. 分词: 调用(无状态)的解析规则的过程. c. 词法分析: 调用(有状态)的解析规则的过程.(判断字符 为一个[独立的词法单元]还是其他词法单元的一部分时,调用的则为有状态的解析规则.) (2) 解析/语法分析; a. 将[词法单元流]转为[抽象语法树(AST)] b. 抽象语法树特性: 则是由元素逐层嵌套所组成.
(3) 生成代码; a. 最后就是把 [抽象语法数(AST)] 生成为可执行代码.
JavaScript 引擎比较复杂. a 在[词法分析] 和 [代码生成] 阶段,会有特定步骤进行优化(对冗余代码优化等).
JavaScript 代码执行前的过程: a. 构建 => 编译3重曲 =(马上)> 代码执行.
负责工作: a. 引擎: 编译 & 执行过程. b. 编译器: 词法分析 & 代码生成. c. 作用域: 收集 & 维护所有声明的标识符组成的一系列查询(确定当前执行代码对标识符的访问权).
吃栗子: E.
var name = 'Rui'
a. 这里实际有2个声明(编译器/引擎各为一). b. 首先,编译器 走到var name
那么先在当前作用域中查找是否有该'name'变量? 如果有则忽略声明,并且继续编译. : 否则编译器则要求作用域在当前作用域集合中声明一个变量,且命名为'name' c. 接着,编译器 为引擎生成运行时所需的代码,代码会被用处理= 'Rui'
的赋值操作. d. 引擎运行操作时,会询问当前所在作用域是否有该变量'name', 如果有,那么使用它; 如果没有,那么往上一层作用域继续找(直到全局Global). e. 最后小结: 变量赋值操作为2个动作..编译器在当前作用域生成该变量(如之前无声明),接着引擎运行时,在当前作用域作为起点从内往外往找该变量(找到则为其赋值,反之抛错).编译器术语: a. LHS查询: b. RHS查询:(Retrieve His Souse Value) c. 个人简单可理解为: Left左边赋值, Right右边取值. E1.
console.log('Rui')
对 'Rui' 使用RHS引用.(取值) E2.let name = 'Rui'"
对 'name' 使用LHS引用.(赋值 | 不关心'name'的值,只想为赋值操作(= 'Rui'
)找到一个目标.) E3. 思考下面的引用过程: E3.a 引用RHS查询 getName 的值.(以为getName需被执行) E3.b 引用LHS查询 对参数 name 进行赋值.(='Rui'
) 隐式赋值. E3.c 引用RHS查询 对 console.log(...) 进行取值,并检查取到的值中是否存在方法:log
(console.log()本身也需一个引用才可执行.). E3.d 引用RHS查询 对参数 name 进行取值.(紧接传于console.log(xxx)). E3.e 有一点需要注意: 如果把上面函数换个写法:let getName, getName = function(name){...}
那么是不是就可以认为它其实也是使用了 LHS查询 引用? 其实不然..因为编译器可在代码生成时同时处理 [声明] & [值] 的定义.且引擎执行代码时,不会有线程专门用来为一个函数值"分配给" getName ...所以不合适也不成立.区分 LSH & RSH: a. 如果变量没有声明的情况下,两种查询是不一样的. b. RSH查询 在所有作用域中找不到变量(未声明).那么引擎会抛错
ReferenceError
(非常重要的异常类型) c. LSH查询 在所有作用域中找不到变量时,那么会在全局作用域中创建声明该变量(自己造一个)..并返回给引擎.(该情况在严格模式下,会抛出ReferenceError
错误.) d. 当RHS查询到一个变量,但我尝试对它的值进行不合理的操作(如:非函数使用函数调用/引用null
或undefined
的属性,则会抛出TypeError
) e. 小结:ReferenceError
: 指作用域中判别失败..TypeError
: 作用域判别成功,但对目标结果的操作非法不合理的.