前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《你不知道的JavaScript(上)之作用域》读书笔记

《你不知道的JavaScript(上)之作用域》读书笔记

原创
作者头像
after the rain
发布2022-08-08 16:12:26
4860
发布2022-08-08 16:12:26
举报
文章被收录于专栏:《你不知道的JS》读书笔记

一、什么是作用域?

1.1定义

程序设计的概念:一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

1.2作用

我们知道任何JS代码在执行前都需要经过编译器(JS引擎)编译 举个简单的例子:

var a = 1;这个简单的JS语句会经过哪些过程呢

第一步:编译器

1.分析代码是否有语法错误

2.解析语法如上例:会被解析成逐级嵌套的抽象语法树 顶级节点为VariableDeclaration,子节点identifier值为a,另一个子节点为AssignmentExpression(也就是我们平时写的augments)该节点的子节点为NumbercLiteral,值为2;

3.代码生成 将AST转换为计算机可执行的代码也就是一组机器指令;如上例会在机器内分配一块内存存储一个名叫a的变量 第二步:JS引擎 负责编译执行JS程序段

其中执行之前会在编译过程中会去JS作用域寻找变量是否声明,是否可被访问,如果寻找不到或是不可被访问,则会抛出程序异常

查找变量的过程

JS引擎执行代码时会对变量进行查找,查找过程由作用域进行协助

主要有两种查找类型LHS(左侧查询)、RHS(右侧查询) 如上例 var a = 1 变量出现在赋值左侧,所以采用LHS查询,如果变量出现在赋值右侧或者是单纯引用如console.log(a)则采用RHS查询

1.3作用域嵌套

作用域是根据变量名称查询变量的一套规则,JS分为全局作用域、局部(函数)作用域、块级作用域

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域嵌套。所以此时在当前作用域无法找到变量时,引擎就会在外层嵌套的作用域中继续查找直到找到该变量,或是抵达最外层作用域(全局作用域)为止。

举例:

代码语言:javascript
复制
function func(a){
   console.log(a + b);
}
var b = 1;

func(2);// 3

对变量b进行RHS引用无法在函数作用域func内部完成,此时就会向上级作用域继续查找,func的上级作用域为全局作用域。

遍历嵌套的作用域链的规则时,引擎从当前的执行作用域查询变量,找不到时,会向上一级继续查找,找到顶层作用域即全局,就会停止查询,这个查询过程可以理解为JS多维数组的遍历过程。从最里层开始遍历查询直至最外层数组遍历结束。

1.4异常

LHS和RHS在调用过程会抛出异常,比如LHS查询不到变量声明时,严格模式下会抛出referenceError标识作用域查询异常,RHS查询到了变量,但对变量执行的操作不符合定义类型,比如对非函数变量进行函数调用,则会抛出TypeError。

二、词法作用域

2.1定义

词法作用域也就是在词法阶段定义的作用域。换句话说,词法作用域就是你在写代码的时候就已经决定了变量的作用域。

注:js中其实只有词法作用域,并没有动态作用域,this的执行机制让作用域表现的像动态作用域,this的绑定是在代码执行的时候确定的。

2.2欺骗词法

修改(欺骗)词法作用域的两种机制:eval 以及 with。但是欺骗词法作用域会导致性能下降。

eval

原理:在执行 eval(…) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。

代码语言:javascript
复制
function foo(str, a) {
    eval( str ); // 欺骗!
    console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

1、eval(…) 调用中的 “var b = 3;”,实际上在 foo(…) 内部创建了一个变量 b,遮蔽了外部(全局)作用域中的同名变量。从而修改了 foo(…) 的词法作用域

2、当 console.log(…) 被执行时,会在 foo(…) 的内部同时找到 a 和 b,但是永远也无法找到外部的 b。

注:如果在严格模式下,eval是有自己独立的词法作用域的,无法修改自身所在作用域

with

with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

代码语言:javascript
复制
var obj = {a: 1,b: 2,c: 3}
obj.a = 2;
obj.b = 3;
obj.c = 4;
with (obj) {a = 2;b = 3;c = 4;}
代码语言:javascript
复制
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 被泄漏到全局作用域上了!)

1、上面例子创建了 o1 和 o2 两个对象,o1 有 a 属性, o2 没有。 2、foo() 函数接收一个 obj 的参数,该参数是一个对象引用,并对这个对象执行了 with(obj) {}。 3、在 with 块内部,a = 2 实际上就是一个LHS引用,并将 2 赋值给 变量 a。 4、o1 传递进去,a=2 赋值操作找到了 o1.a 并将 2 赋值给它 5、o2 传递进去,o2 并没有 a 属性,o2.a 保持 undefined。

性能 JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。

但如果引擎在代码中发现了 eval(…) 或 with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道 eval(…) 会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。 小结:这样的实现方式会导致代码运行变慢,因为复杂了作用域的查找过程,所以不要使用这两种语法。

三、函数作用域和块作用域

3.1函数作用域

定义:函数标识符会创建属于自身的作用域

根据前面作用域查找的规则,是由内向外查找,并不会由外向内查找

代码语言:javascript
复制
function foo(a){
   var b = 2;
  //执行语句
  function bar(){
    var c = 3; 
    //执行语句
  }
}
bar();//失败
console.log(a,b,c)//失败

因为bar函数属于foo函数作用域内的函数变量,所以在全局作用域下调用自然会查找失败,全局作用域不会向foo作用域去申请访问或者是查询,变量a,b在foo作用域下,c在bar作用域下,同理在foo函数下去访问c变量一样会报错;

3.2隐藏内部实现

定义:先声明一个函数,在函数中定义变量或函数,利用函数作用域隐藏代码。

隐藏变量或函数的好处是什么?

假如我们都将变量函数等定义到全局作用域上会产生很多的问题,比如变量污染,垃圾回收性能等问题

在软件设计中有一个原则叫最小特权原则,意思是应该最小限度地暴露必要的内容,而将其他内容都隐藏起来,比如API设计,只暴露函数的调用,而不会暴露定义的变量等;所以JS采用块级作用域,函数作用域来包裹变量。这个和java类设计思想相似,JS ES6语法中class设计思想也是如此。

规避冲突,可以避免同名标识符,比如两个相同名字的标识符但用途却不一样,可以规避这种命名冲突。

3.3匿名函数和具名函数

代码语言:javascript
复制
// 具名
function func(){
    // 代码块
}
// 匿名
setTimeout(function(){
   //代码块
},1000)

1.函数声明是不可以匿名的

2.匿名函数在栈追踪中不会显示出有意义的函数名,所以调试起来很困难

3.如果没有函数名,函数需要引用自身时,只能使用已经过期的arguments.callee引用,比如递归场景

4.匿名函数省略了对于代码的可读性

小结:如果该函数有含义尽量使用具名函数。

3.4块级作用域

代码语言:javascript
复制
for(let i = 0;i<10;i++){
    console.log(i)
}

if(true){
 let a = 0;
 console.log(a);
}

with(){

}
try{
 let a = 0;
}catch(error){
  console.log(error)
}

如上例,for、if、with、try/catch、let定义等都是块级作用域

let不会引起变量提升,在未用let定义前的变量引用会引起报错

块级作用域对于我们编码中是非常有用的,有利于内存垃圾回收

四、提升

代码语言:javascript
复制
a = 2;
var a;
console.log(a); 

a会被打印为2,大部分情况下JS代码都是由上到下依次执行语句,但是由于a变量已经被赋值所以a=2会在全局作用域使用RHS查询a变量的定义所以真实的执行顺序为 var a; a = 2;console.log(a);

代码语言:javascript
复制
console.log(a);// undefined
var a = 2;

console.log(a)语句会先RHS查找定义所以执行顺序为var a;console.log(a);a=2;

小结,所以JS是先定义后赋值的,var定义变量会产生变量提升,但是只针对声明,而赋值语句和执行语句则会停留在当前位置被执行;

函数优先原则

代码语言:javascript
复制
foo();//1
function foo (){
  console.log(1);
}
var foo = function(){
  console.log(2);
}

尽管var关键字定义了同名函数变量,但函数声明优先于变量声明,所以打印1;如果函数声明被重复声明则取最新定义

代码语言:javascript
复制
foo();//2
function foo(){
   console.log(1);
}
function foo(){
   console.log(2); 
}

不推荐这样的覆盖定义,因为无论从代码规范和可读性,这样都会产生很多问题,是一种不好的用法,可以推荐重写function,但不可以重复定义函数,也不推荐重复定义变量;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • eval
  • with
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档