Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

重学js —— 块语句、声明和变量语句、空语句以及表达式语句 #81

Open
lizhongzhen11 opened this issue Feb 29, 2020 · 0 comments
Labels
js基础 Good for newcomers 重学js 重学js系列 规范+MDN

Comments

@lizhongzhen11
Copy link
Owner

lizhongzhen11 commented Feb 29, 2020

块语句、声明和变量语句、空语句以及表达式语句

先看下以下代码,文末解惑:

// 浏览器环境下测试
var a = 'a';
let b = 'b';
age = 21;
console.log(this.a); // 'a',绑定到了window上
console.log(this.b); // undefined
console.log(this.age); // 21,严格模式抛错

1. block

// 注意:函数声明也会被限制在它的语句块内,但是和let, const有区别
// let 
{
  let a = 'a';
}
console.log(a) // ReferenceError: a is not defined

// 函数声明,这里表现和有条件的创建函数类似
foo('top outside...'); // TypeError: foo is not a function
{
  function foo(info) {
    console.log(info)
  }
  foo('inside...'); // inside...
}
foo('bottom outside...'); // bottom outside...

求值

Block : { }

  1. 返回 NormalCompletion(empty)

Block : { 语句列 }

  1. 定义 oldEnv运行时执行上下文LexicalEnvironment
  2. 定义 blockEnvNewDeclarativeEnvironment(oldEnv) (PS:这个算法会返回一个新的词法环境,新的词法环境的外部词法环境引用 oldEnv。算是块的内部作用域)
  3. 执行 BlockDeclarationInstantiation(语句列, blockEnv)
  4. 运行时执行上下文LexicalEnvironment 设置为 blockEnv。 (此时在块的内部作用域中执行)
  5. 定义 blockValue 为 语句列的 求值 结果
  6. 运行时执行上下文LexicalEnvironment 设置为 oldEnv。 (块内部作用域任务完成,回到上一层作用域)
  7. 返回 blockValue

无论控制 如何离开“块”,词法环境始终会恢复为其以前的状态。

语句列 : 语句列 语句列元素

  1. 定义 sl 为 语句列的 求值 结果
  2. ReturnIfAbrupt(sl)
  3. 定义 s 为 语句列元素的 求值 结果
  4. 返回 Completion(UpdateEmpty(s, sl))

语句列的值是语句列中最后一个产生值的元素的值。

// 以下代码全都输出1
{
  1;;;;;
};

{
  1;
  {};
};

{
  1;
  var a;
}

BlockDeclarationInstantiation ( code, env )

当块或 CaseBlock 求值时,会创建一个新声明的 环境记录,在该 环境记录 中实例化块作用域内的声明的每个变量,常量,函数和class的绑定

  1. 定义 envRecenv环境记录
  2. 断言:envRec 是一个声明性的 环境记录
  3. 定义 declarationscodeLexicallyScopedDeclarations
  4. 遍历 declarations 中的每个元素用 d 表示,
    1. 遍历 dBoundNames 中每个元素用 dn 表示

      1. 如果 d 是常量声明,

        1. 执行 ! envRec.CreateImmutableBinding(dn, true)
      2. 否则

        1. 执行 ! envRec.CreateMutableBinding(dn, false)
    2. 如果 d 是 函数声明,Generator声明,异步函数声明,异步Generator声明

      1. 定义 fndBoundNames 唯一元素
      2. 定义 fod实例化函数对象,带有参数 env
      3. 执行 envRec.InitializeBinding(fn, fo)

2. 声明和变量语句

// 注意,在全局环境中使用let,const声明的变量,并不会绑定到全局对象上
age = 21
var a = 'a'
let b = 'b'
const c = 'c'
console.log(this.age) // 21,严格模式下抛错
console.log(this.a) // 'a'
console.log(this.b) // undefined
console.log(this.c) // undefined

let 和 const声明

注意:letconst 声明定义了在 运行时执行上下文LexicalEnvironment 作用域内的变量。变量在包含它们的 词法环境 实例化时创建,但是在它们的 词法绑定 求值前不能获取到。词法绑定 求值时进行变量赋值,而不是变量创建时,由带有 初始化器词法绑定 定义的变量将被赋给其 初始化器赋值表达式 的值。如果在 let 声明里 词法绑定 没有 初始化器,那么当 词法绑定 求值时该变量会被赋值为 undefined

求值

1. 词法声明 : LetOrConst 绑定列表 ;

  1. 定义 next 为 绑定列表 求值结果
  2. ReturnIfAbrupt(next)
  3. 返回 NormalCompletion(empty)

2. 绑定列表 : 绑定列表 , 词法绑定

  1. 定义 next 为 绑定列表 求值结果
  2. ReturnIfAbrupt(next)
  3. 返回 词法绑定 的求值结果

3. 词法绑定 : 绑定标识符

  1. 定义 lhsResolveBinding(绑定标识符的字符串值)
  2. 返回 InitializeReferencedBinding(lhs, undefined)

静态语义规则可确保这种形式的词法绑定不会在 const 声明中发生。

4. 词法绑定 : 绑定标识符 初始化器

  1. 定义 bindingId 为绑定标识符的字符串值
  2. 定义 lhsResolveBinding(bindingId)
  3. 如果 初始化器 是匿名函数,
    1. 定义 value 为带有参数 bindingId 的初始化器的 NamedEvaluation
  4. 否则,
    1. 定义 rhs 为初始化器求值
    2. 定义 value? GetValue(rhs)
  5. 返回 InitializeReferencedBinding(lhs, value) (PS:初始化绑定)

5. 词法绑定 : 绑定模式 初始化器

  1. 定义 rhs 为初始化器求值
  2. 定义 value? GetValue(rhs)
  3. 定义 env运行时执行上下文LexicalEnvironment
  4. 返回 执行使用参数 valueenv 的绑定模式 绑定初始化 的结果

3. 变量语句

注意:var 语句在 运行时执行上下文变量环境 中声明变量。var 变量被创建当其包含的 词法环境 实例化且创建时会初始化为 undefined注意这里与let, const区别,var创建时就会初始化!!!)。在任何 变量环境 作用域内,一个公共的 绑定标识符 可能会出现在多个 变量环境 中,但是这些声明仅共同定义了一个变量。当 变量声明 执行时变量被定义值而不是变量创建时定义值,通过带有 初始化器变量声明 定义的变量会被赋值为其 初始化器 赋值表达式 的值。

求值

变量语句 : var 变量声明列表

  1. 定义 next 为 变量声明列表求值结果
  2. ReturnIfAbrupt(next)
  3. 返回 NormalCompletion(empty)

变量声明列表 : 变量声明列表 , 变量声明

  1. 定义 next 为 变量声明列表求值结果
  2. ReturnIfAbrupt(next)
  3. 返回 变量声明求值结果

变量声明 : 绑定标识符

  1. 返回 NormalCompletion(empty)

变量声明 : 绑定标识符 初始化器

  1. 定义 bindingId 为绑定标识符的字符串值
  2. 定义 lhs? ResolveBinding(bindingId)
  3. 如果 初始化器是匿名函数,
    1. 定义 value 为 参数是 bindingId 的初始化器的 NamedEvaluation
  4. 否则,
    1. 定义 rhs 为初始化器的求值
    2. 定义 value? GetValue(rhs)
  5. 返回 ? PutValue(lhs, value)

注意:如果 变量声明 嵌套在一个 with 语句中且其绑定标识符和 with 语句对象的 环境记录 绑定对象 属性名 相同,步骤5会给该属性赋值为 value 而不是 标识符 绑定的 变量声明

变量声明 : 绑定模式 初始化器

  1. 定义 rhs 为初始化器的求值
  2. 定义 rval? GetValue(rhs)
  3. 返回执行参数为 rvalundefined绑定模式绑定初始化 结果

4. 解构绑定模式

5. 空语句

6. 表达式语句

回顾开头代码

为什么var声明的变量和没有声明直接赋值的变量能绑定到window上,而let, const不行?

即使看了以上 var 以及 let/const 的规范相关定义,好像也无法得到这个问题的答案。虽然 var运行时执行上下文变量环境 中声明变量,而 letconst运行时执行上下文LexicalEnvironment 内声明变量,但是并没有说明我提出的这个问题。

别急,看这里 GlobalDeclarationInstantiation(在Scripts and Modules那一章,太散了),它内部调用了 CreateGlobalVarBinding(vn, false),这个算法会把全局环境的 var 声明绑定到全局对象上!

针对没有声明却赋值的变量在严格模式下会报错,见 C The Strict Mode of ECMAScript

找了整整两天啊,各种关键词都试过了,终于刚刚灵机一动,让我找到了未声明但赋值变量的蛛丝马迹!

age = 21;

这段代码其实是 赋值语句 啊!!!

先看它的求值算法,age 既不是 对象字面量 也不是 数组字面量,最后会走 ? PutValue(lref, rval),而 PutValue 内部判断 lref 是不是 IsUnresolvableReference,事实上,的确是,因为根本没有这个变量!!!

接下来,看下算法内部我需要关注的地方:

  • 定义 globalObjGetGlobalObject (拿到当前领域记录的 [[GlobalObject]] 对象,根据这里的示例代码,这里正好是全局对象)
  • 执行 Set(globalObj, GetReferencedName(V), W, false).

其实 Set 最终会调用对象的 [[Set]](GetReferencedName(V), W, globalObj),其本质 最终调用 OrdinarySetWithOwnDescriptor(o, GetReferencedName(V), W, globalObj, undefined),ownDesc 这里是 undefined,内部算法定义的 parent 也是 null,所以最终会返回 { [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true } !!!

以上步骤才是隐式声明变量的关键,然后回到示例代码,对其赋值 21

2020-03-15 补充

// 来自高级前端面试,我在这里错过一次
// 第二次虽然选对了,但是一时没有想起来错在哪
const emojis = ['1', '2', '3', '4'];
emojis.push(''); // 第 1 步
emojis.splice(0, 2); // 第 2 步
emojis = [...emojis, '']; // 第 3 步
emojis.length = 0 // 第 4 步

// 问上面哪一步会引起错误,选D
A. 1
B. 1 and 2
C. 3 and 4
D. 3

主要是迷惑了,以为考的是数组相关知识,实际上考的是 const 声明不能修改!!!

2020-03-21 补充

补充个经典面试题,输出什么:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1);
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1);
}

2020-07-18 补充

这个算是 冗余声明(Redundant Declarations) 问题,今天看 《JavaScript 20 年》中文版 —— 迷惑行为与 Bug 发现的,以前没见过这个问题,即使看了规范和MDN也没意识到这个问题:

function f(x, x) { // x 对应第二个形参,忽略第一个 x
  var x; // 和第二个形参相同的绑定
  for (var x in obj) { // 和第二个形参相同的绑定
    var x=1, x=2; // 和第二个形参相同的绑定
  }
  var x=3; // 和第二个形参相同的绑定
  console.log(x)
}
var obj = {a: 'a', b: 'b'}

f('c', 'd') // 3

再看看下面代码:

function test(x) {
  var x;
  console.log(x);
}
test('aaa') // 'aaa'

同时需要注意 functionvar 的优先级,虽然都有变量提升功能,但是 var 的优先级比 function 高,这就造成了它们两同时声明同一种变量名时,function 声明会覆盖 var

var test;
function test() {}
test // function

function tt(){}
var tt;
tt // function
@lizhongzhen11 lizhongzhen11 added js基础 Good for newcomers 重学js 重学js系列 规范+MDN labels Feb 29, 2020
@lizhongzhen11 lizhongzhen11 changed the title 重学js —— 块语句、声明和变量语句、空语句以及表达式语句(未完成) 重学js —— 块语句、声明和变量语句、空语句以及表达式语句 Mar 2, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js基础 Good for newcomers 重学js 重学js系列 规范+MDN
Projects
None yet
Development

No branches or pull requests

1 participant