JavaScript核心原理解析(更新到第四节)


文章目录

  • JavaScript核心原理解析(更新到第四节)
  • 第一节 如何解决语言问题
  • 第二节 delete 0:JavaScript中到底有什么是可以销毁的
  • 第三节 var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
  • 第四节 a.x=a={n2}:一道被无数人无数次地解释过的经典面试题
  • 第七节 x: break x



第一节 如何解决语言问题

1、JS是一门多范型语言,或者,也称为混合范型语言


第二节 delete 0:JavaScript中到底有什么是可以销毁的

1、对象和数组的字面量语法长这样:

let obj = {
	niko: 'shot'
}

2、所有删除(不是引用)的 delete 就直接返回 true

3、delelet 这个操作的正式语法设计并不是“删除某个东西”,而是“删除一个表达式的结果

4、:单值表达式(没有运算符参与的表达式)的运算结果返回那个“对象字面量”的单值

delete {} //这里其实也是值,因为是表达式运算

delete运算发现它的操作数是“值 / 非引用类型”,就直接返回了 true。所以,什么也没有发生

5、归根到底

delete x

是在删除一个表达式的、引用类型的结果(Result)

总结:
delete 运算符尝试删除值数据时,会返回 true,表示没有错误(Error)。
delete 0 的本质是删除一个表达式的值(Result)。
delete x 与上述的区别只在于 Result 是一个引用(Reference)。
delete 其实只能删除一种引用,即对象的成员(Property)。


第三节 var x = y = 100:声明语句与语法改变了JavaScript语言核心性质

1、变量声明后的初始化

JavaScript 环境在变量声明(varDelcs)【比如创建一个“变量名(varName in varDecls)”】后,会为它初始化绑定一个 undefined 值。

而”词法名字(lexicalNames,比如let创建的变量)”在创建之后就没有这项待遇,所以它们在缺省情况下就是“还没有绑定值”的标识符。

如果提前去访问他们还会被拒绝(但它们这时候其实已经在上下文环境里存在了)

2、向一个不存在的变量赋值

赋值的本质:将右操作数(的值)赋给左操作数(的引用)

一个赋值表达式的左边和右边其实“都是”表达式! “= ”是初始化器,不是一个严格意义上的“赋值运算”

x = y = 100

x被严格地称为“赋值表达式的左手端(lhs)操作数”,是表达式

而var x只是声明

全局环境的本质:在早期设计中,JavaScript 的全局环境是引擎使用一个称为“全局对象”的东西管理起来,这个全局对象几乎类似或完全等同于一个普通对象。

(JavaScript 引擎将全局的一些缺省对象、运行期环境的原生对象等东西都初始化在这个全局对象的属性中)


不命名直接赋值:JavaScript 将变量名作为属性名添加给全局对象,我们可以通过delete删除这个对象上的属性。


ECMAScript的优化:ECMAScript 规定在这个全局对象之外再维护一个变量名列表(varNames),所有在静态语法分析期或在 eval() 中使用var声明的变量名就被放在这个列表中。然后约定,这个变量名列表中的变量是“直接声明的变量”,不能使用delete删除

var a = 100; 
  x = 200;
> a不能删除,x可以被删除
  delete a 
> false 
  delete x 
> true

> 当var声明发生在 eval() 中的时候
  eval('var b = 300')
> 它的性质是可删除的,这种情况下使用var声明的变量名也会添加到 varNames 列表.
> 但它也可以从varNames 中移除(这是唯一一种能从 varNames 中移除项的特例,
> 而 lexicalNames 中的项是不可移除的)
  delete b 
>  true



所以标题中的代码到底发生了什么

var x = y = 100
  1. y = 100,它发生了一次“向不存在的变量赋值”,所以它隐式声明了一个全局变量y,并赋值为 100。
  2. var x = y,它将y=100的这个表达式的结果,赋值给变量申明var x


声明和语句的区别

声明发生在编译期,语句发生在运行期

声明发生在编译期,由编译器为所声明的变量在相应的变量表,增加一个名字。语句是要在运行时执行的程序代码。

因此,如果声明不带初始化,那么可以完全由编译器完成,不会产生运行时执行的代码。

声明不是语句,声明只是在告诉编译器该干嘛,并没有对运行产生影响



何时进行词法分析

在加载.js文件之后,执行第一行用户代码之前,就已经完成了全部的词法分析(当然,除了eval执行的)


第四节 a.x=a={n2}:一道被无数人无数次地解释过的经典面试题

一切都是表达式,一切都是运算。

过程式的,或编译型语言,,所谓“标识符 / 变量”就是一个计算对象,它可能直接表达为某个内存地址指针,或者是一个编译器处理的东西。

a = 100 
b * c

传统理解:
第一行理解为“a 有了值 100”;
第二行理解为“b 与 c 的乘积”

JavaScript 中,一共有六个操作,第二号举例包括:
1、将b理解为单值表达式,求值并得到GetValue(evalute(‘b’));
2、 将c理解为单值表达式,求值并得到GetValue(evalute(‘c’));
3、将上述两个值理解为求积表达式’*'的两个操作数,计算

标题中“a.x”这个表达式的语义是:
表达式“a.x”的计算结果是一个引用,因此通过这个引用保存了一些计算过程中的信息——例如它保存了“a”这个对象,以备后续操作中“可能会”作为this来使用。

回到标题中的示例

答案:
有一个新的a产生,它覆盖了原始的变量a,它的值是{n:2};

最左侧的“a.x”的计算结果中的“原始的变量a”在引用传递的过程中丢失了,且“a.x”被同时丢弃。

所以第二次赋值操作“a.x = …”是无意义的。它所操作的对象,也就是“原始的变量a”被废弃了。

但是,如果有如变量、属性或者闭包等,持有了这个“原始的变量a”,那么上面的代码的影响仍然是可见的。

let a = {n:1}, ref = a; 
a.x = a = {n:2}; 
console.log(a.x); // --> undefined 
console.log(ref.x); // {n:2}

总结:
这道面试题与运算符优先级无关;
这里的运算过程与“栈”操作无关;
这里的“引用”与传统语言中的“指针”没有可比性;
这里没有变量泄露;
这行代码与上一讲的例子有本质的不同;
上一讲的例子“var x = y = 100”严格说来并不是连续赋值。

什么是引用
引用就是引用,它就是一个计算的结果,它存放结果中包括的那几个东西。它是一个数据结构,用在引擎层面来存储计算过程的中间信息,以及在连续计算中传递这些信息。

问答
with 只指定属性查找的优先级,所以 with 里面的语句 x = 100 还是会泄漏到全局


第七节 x: break x

问答
1、块级作用域与它内部声明了什么没关系。
例如,一个“块语句{ }”就有一个块级作用域,哪怕它内部一行代码也没有。

if(1) {
	let b=10
}

上述语句中有两个语句:
一个是if语句本身,它是个“单语句”(ECMAScript就是这么定义的,NodeJS的错误提示里也有),它没有块级作用域
后面的一对大括号“{}”,是一个“块语句”,有一个块