对作用域的理解

1、var变量声明的编译过程

ini 复制代码
var a = 2;
  • 声明变量a:查看作用域中是否已有同名变量,如有则忽略该声明。同一个作用域内多次用var声明同一个变量名不会报错。

  • 赋值:查找当前作用域及往上的作用域链,找到a后赋值,如果没找到,就会在最上层的作用域声明变量a后赋值

    function fn() { // var a = 3 // 声明fn内的局部变量a a = 3 // 声明全局变量a }

2、LHS查询和RHS查询

作用域是一套规则,用于确定在何处、如何查找变量,查找方式可分为两种类型:LHS查询和RHS查询。

LHS查询:查找以供赋值

RHS查询:查找以供使用

css 复制代码
function foo(a) {
  var b = a
  return a + b
}

var c = foo(2)

如以上代码,有3处LHS:c、a(隐式赋值)、b

4处RHS:foo、a、a、b(第一次a是查找后赋值给b,第二次a是查找进行加+操作)

3、为什么要区分LHS查询和RHS查询?

在变量未声明时、即所有作用域都无法找到变量的情况下,两种查询方式进行的行为不同。

css 复制代码
var a = 2
console.log(a + b); // 对b的RHS查询,会报错
b = a // 对b的LHS查询,不会报错

如以上代码,对b进行了一次RHS一次LHS,进行RHS查询时,找不到变量,会抛出ReferenceError异常;进行LHS查询时,找不到变量,会创建变量后返回。(非严格模式)

(严格模式下禁止隐式创建变量,因此在LHS查询失败时也会抛出异常)

扩展:RHS抛出的异常,ReferenceError异常表示作用域判别失败,TypeError异常表示作用域判别成功,但对结果的操作非法或不合理(如对非函数类型进行函数调用)

4、词法作用域

词法作用域也称为静态作用域,在定义时确定,只关注函数在何处声明

scss 复制代码
function foo() {
  console.log(a);
}

var a = 2

function foo2() {
  var a = 3
  foo()
}

foo2() // 2

如上代码,打印值会是2,因为foo会在定义时的作用域内寻找a,最终找到的是全局变量a。

(动态作用域是在运行时确定,关注函数在何处调用的。js中没有,但this机制很像动态作用域。)

5、作用域链

要了解作用域链的概念,需要先知道什么是变量对象

变量对象是指当前作用域内的所有变量、函数、形参,所组成的一个对象

如1中的示例代码,全局变量对象FO为a、fn foo、fn foo2,foo2函数内的局部变量对象AO(active object,因为函数只有调用时才有价值 )为a,foo函数内无变量对象

作用域链是变量对象的集合,由一系列AO+VO组成的链式结构。它规定了代码访问的顺序,只能向上访问,不能向下访问

6、用eval/with修改词法作用域

(1)eval()函数,接收一个字符串作为参数,将其中的内容视为代码执行(不支持es6语法),可用于动态插入代码

非严格模式可欺骗浏览器,修改词法作用域环境

scss 复制代码
function foo(str, a) {
  eval(str);
  console.log(a, b); // 1. 3
}

foo("var b = 3", 1);

如以上代码,var b = 3会直接在eval的位置执行,b会成为foo作用域内的变量。

严格模式下eval运行时会有自己的作用域,b会是eval作用域内的变量,此时在foo打印b就会报错b。

(2)with,可以将对象处理为隔离的词法作用域,只能在非严格模式下使用,严格模式下是被禁止的。

ini 复制代码
var obj = {
  a: 1,
  b: 2,
  c: 3,
};

// 会创建一个obj内的词法作用域
with (obj) {
  a = 3;
  b = 4;
  c = 5;
  d = 6; // 当对象不存在时,变量会被泄漏到with所处的上级作用域中
  var e = 7; // 不会限制在块作用域内,会被添加到with所处的作用域中
}

这两种方法现在很少使用,也不推荐使用,因为它们会对性能造成影响。词法作用域的好处在于js引擎可以在编译阶段就确定所有变量和函数的定义位置,以便在执行时快速查找。而eval/with无法进行这一优化,会拖慢代码运行效率。

7、块级作用域

块级作用域指用{}包裹起来的代码内的作用域,常见的有if else和for循环内的代码块作用域。使用块级作用域的好处有:

(1)垃圾回收机制

ini 复制代码
function process(data) {}

// let data = { a: 1 };
// process(data);

{
  let data = { a: 1 };
  process(data);
}

var btn = document.getElementById("btn");
btn.addEventListener("click", function (evt) {
  console.log("clicked");
});

如上,执行到事件绑定时,如果不显式地声明块级作用域,js引擎会认为上面声明的变量可能仍会在绑定事件中使用,不会回收。而使用块级作用域,引擎就会知道这段代码不需要保留、可以进行回收。

(2)循环时重新绑定

在循环中,当使用let声明变量时,每次循环的i值都是独立的

ini 复制代码
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i); // 01234
  }, 0);
}

这是因为它的内部执行如下,在每次循环的块级作用域内,都会重新声明一个变量绑定赋值,当前i仅在当前次循环的块级作用域内生效。

ini 复制代码
{
  let i
  for (i = 0; i < 5; i++) {
    let j = i
    setTimeout(() => {
      console.log(j); // 01234
    }, 0);
  }
}
相关推荐
木子七1 小时前
Js内建对象
前端·javascript
Front思2 小时前
根据输入的详细地址解析经纬度
前端·javascript
好奇的候选人面向对象2 小时前
v-input-limit
javascript·vue.js·elementui
洪大宇2 小时前
Vuestic 整理使用
开发语言·javascript·ecmascript
zqwang8882 小时前
Performance API 实现前端资源监控
前端·javascript
我看刑2 小时前
el-datepicker此刻按钮点击失效
javascript·vue.js·ecmascript
HC182580858322 小时前
零基础学西班牙语,柯桥专业小语种培训泓畅学校
前端·javascript·vue.js
图扑软件2 小时前
掌控物体运动艺术:图扑 Easing 函数实践应用
大数据·前端·javascript·人工智能·信息可视化·智慧城市·可视化
好青崧3 小时前
JavaScript 循环与条件判断
开发语言·javascript·udp
奶糖 肥晨3 小时前
React 组件生命周期与 Hooks 简明指南
前端·javascript·react.js