《JavaScript核心原理解析》推荐

2020/01/27 JavaScript 共 12911 字,约 37 分钟

重建知识体系

构建前端知识体系,要知其然知其所以然

代码的静态组织

JavaScript 的所谓“脚本代码”,在引擎层面看来,就是一段文本,JavaScript 解析引擎是“逐字符”地处理代码文本的。

JavaScript 是动态语言,表达式是一个很独特的东西,所有一切表达式运算的终极目的都是为了得到一个值(表达式的本质是求值运算

ECMAScript 的目的是描述“引擎如何实现”,而不是“描述语言是什么”。

在任何语句执行并得到结果时,如果它“当时”是一个所谓的“引用”,那么这个引用就必须先调用“GetValue(x)”来得到值,然后放到这个“value”域中去。因为“引用”是一个规范层面的东西,它不是 JavaScript 语言能理解的,也无法展示给开发者。

引用还是指向值、代表值的一个概念,它只是“获得值的访问能力”的一个途径。最终的结果仍然指向原点:计算值、求值。

JavaScript中到底有什么是可以销毁的

delete

引用类型的结果,delete 的删除就是很好的解释

在 JavaScript 的内部,所谓“引用”是可以转换为“值”,以便参与值运算的。因为表达式的本质是求值运算,所以引用是不能直接作为最终求值的操作数的。这依赖于一个非常核心的、称为“GetValue()”的内部操作。所谓内部操作,也称为内部抽象操作(internal abstract operations),是 ECMAScript 描述一个符合规范的引擎在具体实现时应当处理的那些行为,GetValue()是从一个引用中取出值来的行为。

obj.x => 是一个表达式,一切表达式运算的终极目的都是为了得到一个值。obj.x也是一个引用,“属性存取(”.”运算符)”返回一个关于“x”的引用,引用只是在语法层面上表达“它是对某种语法元素的引用”,而与在执行层面的值处理或引用处理没关系,因为最后我们在页面看到的是一个结果,而不是地址的引用

{} 一对大括号是表示一个字面量的对象,当它被作为表达式执行的时候,结果也是一个值,这类表达式称为“单值表达式”的原因,这里并没有所谓的“引用”。可以准确地说,是“非引用类型”。

访问不存在的变量x报ReferenceError错误,其实是对x表达式的的Result引用做getValue的时候报的错误,然后为啥typeof x和delete x不报错,因为这两个操作没有求值。

=> 所有赋值操作的含义,是将右边的“值”,赋给左边用于包含该值的“引用” 。x=x的语义并不是“x 赋给 x”,而是“把值 x 赋给引用 x”

所谓值类型中的字符串是按照引用来赋值和传递引用(而不是传递值)的。如果x的值是1,那么y = x的话,就是把1这个值“抄写”到y里面去。这是“正常的值”的处理方法,但是如果“字符串值”也这么处理,就完蛋了,因为字符串可能无数多个字符,那么当y = x按照“正常的值处理方法”来实现的话,这个“值的复制”的开销就受不了。所以:“值类型中的字符串”,是指照, “引用来赋值和传递引用”的;且,它是只传递引用(而不是传递值)的。因为基本类型的值是不可改变的所以修改的时候就是直接用的新的替换 原始数据

学到了=>:引用类型和基本类型只是在语法上面的表示,在执行层是没有关系的,最后我们看到的是一个结果值而不是一堆引用

声明和赋值

var x=y=100

JavaScript 只有变量和常量两种标识符,(变量:let var function class 常量:const import 还有特殊的for (varletconst x …),try … catch (x)

将可以通过“静态”语法分析发现那些声明的标识符;标识符对应的变量 / 常量“一定”会在用户代码执行前就已经被创建在作用域中。并且因此它会使得当前代码上下文在正式执行之前就拥有了被声明的标识符

JavaScript 是允许访问还没有绑定值的var所声明的标识符的。这种标识符后来统一约定称为“变量声明(varDelcs)”,而“let/const”则称为“词法声明(lexicalDecls)”。JavaScript 环境在创建一个“变量名(varName in varDecls)”后,会为它初始化绑定一个 undefined 值,而”词法名字(lexicalNames)”在创建之后就没有这项待遇,所以它们在缺省情况下就是“还没有绑定值”的标识符=> var 有声明提前,但是let/const 就没有

var x=y=100

x 和 y 是两个不同的东西,前者是声明的名字,后者是一个赋值过程可能创建的变量名.使用“=”这个符号引导了一个初始化语法——通常情况下可以将它理解为一个赋值运算;所有赋值操作的含义,是将右边的“值”,赋给左边用于包含该值的“引用” 。

变量“y”会因为赋值操作而导致 JavaScript 引擎“意外”创建一个全局变量

声明和语句的区别在于发生的时间点不同,声明发生在编译期,语句发生在运行期。声明发生在编译期,由编译器为所声明的变量在相应的变量表,增加一个名字。语句是要在运行时执行的程序代码。因此,如果声明不带初始化,那么可以完全由编译器完成,不会产生运行时执行的代码。

=> 声明语义就是静态语言的处理,执行语义就是动态语言的处理。这是两种语言范型的分水岭

表达式与语句

如果说在语法“var x = 100”中,“= 100”是向 x 绑定值,那么“var x”就是单纯的标识符声明。这意味着非常重要的一点——“x”只是一个表达名字的、静态语法分析期作为标识符来理解的字面文本,而不是一个表达式

在“var 声明”语法中,变量名位置上就是写不成a.x的。

var a.x = … // <- 这里将导致语法出错

所谓“a.x”也是一个表达式,其结果是一个“引用”。

a.x = a = {n:2}

计算单值表达式a,得到a的引用;

将右侧的名字x理解为一个标识符,并作为“.”运算的右操作数;

计算“a.x”表达式的结果(Result)

保存在“a.x”这个引用中的“a”是当前的“{n:1}”这个对象。好的,接下来再继续往下执行:

a = {n:2}

左操作数a作为一个引用被覆盖了,这个引用仍然是当前上下文中的那个变量a。因此,这里真实地发生了一次a = {n:2}。

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

那么现在,表达式最开始被保留在“一个结果(Result)”中的引用a会更新吗?不会的。这是因为那是一个“运算结果(Result)”,这个结果有且仅有引擎知道,它现在是一个引擎才理解的“引用(规范对象)”,对于它的可能操作只有:取值或置值(GetValue/PutValue),以及作为一个引用向别的地方传递等。

let obj={foo(){return this}}

obj.foo()===obj // true // foo在对象上面的引用 eval(‘obj.foo’)()==obj // false 因为eval 返回的是一个结果不支持引用,所以返回foo的时候就返回了它本身

执行语句:eval(‘obj.foo’) 返回的是值,命令式范型的体现

执行表达式: obj.foo() 可以返回的是一个引用,函数式范型的体现

模块装载

ECMAScript 6 模块是静态装配的,而传统的 Node.js 模块却是动态加载的

export ,在导出的时候,其实是先在“某个名字表”中登记一个“名字 x”就可以了。这个过程也就是 JavaScript 在模块装载之前对 export 所做的全部工作,

JavaScript 就可以依据所有它能在静态文本中发现的import语句来形成模块依赖树,最后就可以找到这个模块依赖树最顶端的根模块,并尝试加载之。

后续的装配过程,找到并遍历模块依赖树的所有模块(这个树是排序的),然后执行这些模块最顶层的代码。直到所有模块的顶层代码都执行完毕,那么所有的导出名字和它们的值也都必然是绑定完成了的

所谓模块的装配过程,就是执行一次顶层代码而已。

找到并遍历模块依赖树的所有模块(这个树是排序的),然后执行这些模块最顶层的代码

导出名字与导出值本质上并没有差异,在静态装配的阶段,它们都只是表达为一个名字而已。

export …语句通常是按它的词法声明来创建的标识符的,例如export var x = …就意味着在当前模块环境中创建的是一个变量,并可以修改等等。但是当它被导入时,在import语句所在的模块中却是一个常量,因此总是不可写的=> B模块中export一个let变量,然后在A模块中import它为x。然后你尝试在A模块中x++,你会发现提示为常量不可写。

所以A、B两个模块中的名字其实并不是同一个变量,它们名字相同(或者不同),但A模块中只是通过一个(类似于别名的)映射来指向B模块中的名字.

如果是引用类型的话因为引用的是同一个地址。所以是可以修改的

块级作用域

越少作用域的执行环境调度效率也就越高,执行时的性能也就越好

所谓的“块级作用域”有两种形式,一种是静态的词法作用域,另一种动态的、“块级作用域”的实例来说,只有当存在潜在标识符冲突的时候,才有必要新添加一个作用域来管理它们

在早期的javascript中 由于作用域只有上面两个,所以任何一个“var 声明”的标识符,要么是在函数内的,要么就是在全局的,没有例外。按照这个早期设计,如下语句中的变量x:

for (var x = …) 里面的变量i被提升到了全局的作用域中

循环语句(对于支持“let/const”的 for 语句来说)“通常情况下”只支持一个块级作用域。在 JavaScript 引擎实现“支持 let/const 的 for 语句”时,就在这个地方做了特殊处理:为循环体增加一个作用域。如果将 for 语句的块级作用域称为 forEnv,并将上述为循环体增加的作用域称为 loopEnv,那么 loopEnv 它的外部环境就指向 forEnv。

于是在 loopEnv 看来,变量i其实是登记在父级作用域 forEnv 中,并且 loopEnv 只能使用它作为名字“i”的一个引用。更准确地说,在 loopEnv 中访问变量i,在本质上就是通过环境链回溯来查找标识符(Resolve identifier, or Get Identifier Reference)。 形成了一个闭包。

for (let i in [1, 2])setTimeout(() => console.log(i), 1000) // 0,1

这个例子创建了一些定时器。当定时器被触发时,函数会通过它的闭包(这些闭包处于 loopEnv 的子级环境中)来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个 for 迭代有可能都已经结束了。

这个 loopEnv 就必须是“随每次迭代变化的”。也就是说,需要为每次迭代都创建一个新的作用域副本,这称为迭代环境(iterationEnv)。因此,每次迭代在实际上都并不是运行在 loopEnv 中,而是运行在该次迭代自有的 iterationEnv 中。也就是说,在语法上这里只需要两个“块级作用域”,而实际运行时却需要为其中的第二个块级作用域创建无数个副本。

“循环与函数递归在语义上等价”。所以在事实上,上述这种 for 循环并不比使用函数递归节省开销

代码的动态执行

如何判断一个对象是否可迭代或者让一个对象变得可迭代(有Symbol.iterator 这个属性,并且属性有值)

JavaScript 的执行机制包括“执行权”和“数据资源”两个部分,分别映射可计算系统中的“逻辑”与“数据”

而块级作用域(也称为词法作用域)以及其他的作用域本质上就是一帧数据,以保存执行现场的一个瞬时状态(也就是每一个执行步骤后的现场快照)。而 JavaScript 的运行环境被描述为一个后入先出的栈,这个栈顶永远就是当前“执行权”的所有者持用的那一帧数据,也就是代码活动的现场。

作用域退出,就是函数 RETURN。作用域挂起,就是执行权的转移。作用域的创建,就是一个闭包的初始化。

“离开语句”意味着清除语句所持有的一切资源,如同函数退出时回收闭包。但是,这也同样意味着“语句”中发生的一切都消失了

函数是求值,所以返回的是对该函数求值的结果(Result),该结果或是值(Value),或是结果的引用(Reference)。而语句是命令,语句执行的返回结果是该命令得以完成的状态(所以语句会有引用丢失的问题

可执行结构:语句,表达式,剩余参数,参数展开,赋值模板,参数赋默认值(剩余参数唯一能动态创建和指定参数个数的语法,可以替换 foo.apply()

赋值模板,都是在语法解析期就被分析出来,并在 JavaScript 内部作为一个可执行结构存放着。然后在运行期,会用它们来完成一个“从右操作数按模板取值,并赋值给左操作数”的过程。

obj.foo,它被称为属性引用(Property Reference)。属性引用不是简单的标识符引用,而是一个属性存取运算的结果,JavaScript 并不是静态分析的,因此它无法在语法阶段确定“obj.foo”是不是一个函数,也不知道用户代码在得到“obj.foo”这个属性之后要拿来做什么用。

直到运行期处理到下一个运算(例如上面这样的运算时),JavaScript 引擎才会意识到:哦,这里要调用一个方法。

然而,方法调用的时候是需要将 obj 作为 foo() 函数的 this 值传入,这个信息只能在上一步的属性存取“obj.foo”中才能得到。所以 obj.foo 作为一个属性引用,就有责任将这个信息保留下来,传递给它的下一个运算。只有这样,才能完成一次“将函数作为对象方法调用”的过程。引用作为函数调用(以及其它某些运算)的“左操作数(lhs)”时,是需要传递上述信息的。这也就是“引用”这种可执行结构的确定逻辑。

在引擎层面,如果一个过程只是将“查找的结果展示出来”,那么它最终就表现为值;如果包括这个过程信息,通常它就表现为引用。

模板字面量的内部结构中,主要包括将模板多段截开的一个数组,原始的模板文本(raw)等等。在引擎处理模板时,只会将该模板解析一次,并将这些信息作为一个可执行结构缓存起来(以避免多次解析降低性能),此后将只使用该缓存的一个引用。当它作为字面量被取值时,JavaScript 会在当前上下文中计算各个分段中的表达式,并将表达式的结果填回到模板从而拼接成一个结果,最后返回给用户。

let x = 'Hi', y = 'Kevin';
var res = message`${x}, I am ${y}`;
console.log(res);
我们可以自定义 message 函数来处理返回的字符串:

// literals 文字
// 注意在这个例子中 literals 的第一个元素和最后一个元素都是空字符串
function message(literals, ...value2) {
	console.log(literals); // [ "", ", I am ", "" ]这里是所有被变量切割的字符串组成的数组
	console.log(value1); // 这里是所有变量的组成的数组
}

函数的执行过程

函数的三个语义组件

参数:函数总是有参数的,即使它的形式参数表为空; 执行体:函数总是有它的执行过程,即使是空的函数体或空语句; 结果:函数总是有它的执行的结果,即使是 undefined。

每个实例 / 闭包都有一个自己独立的运行环境,也就是运行期上下文。JavaScript 中的闭包与运行环境并没有明显的语义差别,唯一不同之处,仅在于这个“运行环境”中每次都会有一套新的“参数”,且执行体的运行位置(如果有的话)被指向函数代码体的第一个指令。

闭包的作用与实现方法都与“for 循环”中的迭代环境没有什么不同,命令式语句(for)和函数式语言,是采用相同的方式来执行逻辑的。只不过前者把它叫做 iteratorEnv,是 loopEnv 的实例;后者把它叫做闭包,是函数的实例

函数参数的三种非简单参数类型(缺省参数=>给参数一个默认值 如果一个参数为null,他表示这个参数被赋值了一个null、剩余参数=>…rest和模板参数=> ${name}),不能有重名参数出现,“非简单参数”时,需要通过“初始器赋值”来完成名字与值的绑定。

// 一般函数声明
function f(x) {
  console.log(x);
}

// 表达式`a=100`是“非惰性求值”的,相当于f(100) 相当于是var
f(a = 100);

在这个示例中,传入函数f()的将是赋值表达式a = 100完成计算求值之后的结果。考虑到这个“结果”总是存在“值和引用”两种表达形式,所以 JavaScript 在这里约定“传值”。于是,上述示例代码最终执行到的将是f(100)。

在“缺省参数”的语法设计里面,undefined 正好是一个有意义的值,它用于表明参数表指定位置上的形式参数是否有传入,所以参数 undefined 也就不能作为初值来绑定,如果“用undefined作为(绑定的)初值了”,那么还能分得清一个参数到底是赋值为undefined了,还是初值为undefined么?这就导致了使用“初始器”的参数表中,所对应那些变量是一个“无初值的绑定”。因此如果这个“初始器”(我是指在它初始化的阶段里面)正好也要访问变量自身,那么就会导致出错了。而这个出错过程也就与如下示例的代码是一样的,并且也导致一样的错误 f = (x = x) => x;

递归将循环的次数直接映射成函数“执行体”的重复次数,将循环条件放在函数的参数界面中,并通过函数调用过程中的值运算来传递循环次数之间的数值变化。递归作为语义概念简单而自然,唯一与函数执行存在(潜在的)冲突的只是所谓栈的回收问题,亦即是尾递归的处理技巧等,但这些都是实现层面的要求,而与语言设计无关。

迭代也是循环语义的一种实现,它说明循环是“函数体”的重复执行,而不是“递归”所理解的“函数调用自己”的语义。这是一种可受用户代码控制的循环体。迭代中有值 (value) 和状态 (done)”两个控制变量。只调用了多次的 return 语句。

参数展开其实是数组展开的一种应用,而数组展开在本质上就是依赖迭代器的。(拥有Symbol.iterator 这个符号属性有值的对象)

function foo({x, y}) {
  ...
}

for (var {x, y} in obj) {
  ...
}

而所有这些地方的赋值模板,都是在语法解析期就被分析出来,并在 JavaScript 内部作为一个可执行结构存放着。然后在运行期,会用它们来完成一个“从右操作数按模板取值,并赋值给左操作数”的过程。这与将函数的参数表作为样式(Formal)存放起来,然后在运行期逐一匹配传入值是异曲同工的。

(function f() { … })=> 那么它的结果是一个函数类型的“数据”

函数既是可以执行的逻辑,也同时是可以被逻辑处理的数据。回调函数

var arr = new Array;
for (var i=0; i<5; i++) arr.push(function f() {
  // ...
});

在这个例子中,静态的函数f()有且仅有一个;而在执行后,arr[]中将存在该函数f()的 5 个实例,每一个称为该函数的一个运行期的闭包。它们各各不同,例如:

arr[0] === arr[1] false

当函数的参数不是简单类型的时候(缺省参数、剩余参数和模板参数之一的)无论是否在严格模式中,形式参数与 arguments 之间都将解除绑定关系。

在旧的模式中,参数和arguments下标是绑定的,所以:

function f(x) {
  console.log(x); // input - 0
  arguments[0] = 100;
  console.log(x); // 100
  console.log(arguments[0]); // 100
}
f(0);


但是非简单类型参数是,这两个东西是不绑定的。例如:

function f(x = 'a') {
  console.log(x); // input - 0
  arguments[0] = 100;
  console.log(x); // input - 0
  console.log(arguments[0]); // updated - 100
}
f(0);

在使用传统的简单参数时,只需要将调用该参数时传入的实际参数与参数对象(arguments)绑定就可以了;而使用“非简单参数”时,需要通过“初始器赋值”来完成名字与值的绑定。同样,这也是导致“形式参数与 arguments 之间解除绑定关系”的原因。

这与参数的实现方法(两种绑定方式)有关,而与是不是调用时传入了缺省参数——以及其它非简单参数——是无关的。

两种绑定模式的区别在于:通常将实际参数与参数对象绑定时,只需要映射两个数组的下标即可,而“初始器赋值”需要通过名字来索引值(以实现绑定),因此一旦出现“重名参数”就无法处理了。

原型到类

任何对象都有“constructor”这个属性,缺省指向创建它的构造器函数,并且它应当是隐藏的属性,

x = "abc";
console.log(x.toString());

x.toString() 时,JavaScript 会自动将“值类型的字符串(“abc”)”通过包装类变成一个字符串对象,这个包装的过程发生于函数调用运算“( )”的处理过程中,仅仅是“对象属性存取”这个行为本身,并不会触发一个普通“值类型数据”向它的包装类型转换。Undefined 没有自己的包装类

属性存取的不确定性,因为JavaScript 的动态性,属性存取结果还受到原型继承(链)的影响

1 in 1..constructor false


# 修改原型链中的对象
> Number[1] = true; // or anything


# 影响到上例中表达式的结果
> 1 in 1..constructor
true

而“x.constructor”不是自有属性,并且,由于 x 是“Number()”这个类 / 构造器的子类实例,因此该属性实际继承自原型链上的“Number.prototype.construtcotr”这个属性。然后,在缺省情况下,“aFunction.prototype.construtcotr”指向这个函数自身。也就是说,“Number.prototype.construtctor”与“1…constructor”相同,且都指向 Number() 自身。所以上面的示例中,当我们添加了“Number[1]”这个下标属性之后,标题中表达式的值就变了。

  1. 判断对象是否实现 [Symbol.toPrimitive] 属性,如果实现调用它,并判断返回值是否为值类型,如果不是,执行下一步。
  2. 如果转换类型为 string,依次尝试调用 toString() 和 valueOf() 方法,如果 toString() 存在,并正确返回值类型就不会执行 valueOf()。
  3. 如果转换类型为 number/default,依次尝试调用 valueOf() 和 toString(),如果 valueOf() 存在,并正确返回值类型就不会执行 toString()。

数组默认没有实现 [Symbol.toPrimitive] 属性,因此需要考察 2、3 两步。

[] + ’‘ 表达式为 string 转换,会触发调用 toString() 方法,结果为空字符,等价于 ’’ + ‘’ 结果为 ‘’。 +[] 表达式是 number 转换,会先触发调用 valueOf() 方法,该方法返回的是空数组本身,它不属于值类型,因此会再尝试调用 toString() 方法,返回空字符,+‘’ 结果为 0;

作用域通常是语法所对应的块,是静态概念的,而闭包是运行期才使用的概念,函数被调用一次就有一个闭包出现,但函数自身其实只有一个作用域

x instanceof AClass表达式的右侧是一个类名(对于之前的例子来说,它指向构造器 Car

JavaScript 的“类”与“函数”有了明确的区别:类只能用 new 运算来创建,而不能使用“()”来做函数调用。

如果类声明中通过 extends 指定了父类,那么:必须在构造器方法(constructor)中显式地使用super()来调用父类的构造过程;在上述调用结束之前,是不能使用this引用的。显然,真实的this创建就通过层层的super()交给了父类或祖先类中支持创建这个实例的构造过程。这样一来,子类中也能得到一个“拥有父类所创建的带有内部槽的”实例

class Parent { getName() { return this } };
class Son extends Parent { getName() { return super.getName() } };
const s = new Son();
s.getName(); // 会正常打印Son类。

对象,是对数据的封装;

解构,就是从封装的对象中,抽取数据。

“某某编程思想”,一个,是在编程中怎么管理数据,另一个则是怎么组织逻辑

在一个“有限的存储空间”里面,其实只能表达为一个“块”,为所有连续的块添加一个连续的“索引”-索引数组。为所有不连续的块添加一个唯一的“名字”-关联数组(关联数组,给对象加上Symbol.iterator变成可迭代,就可以通过数组遍历出来)

计算的本质是求“值”,因此几乎所有的引用类型呢,最终都会将“与它的相关的运算结果”指向“值”。至于这一切背后的原因,其实也很简单,就是物理的计算系统最终也只能接收“字节、位”等等这样的值类型数据。但是在高级语言中,或者应用编程中呢,程序员又需要高层级的抽象来简化编程,所以才会有结构体,以及我们在这里讲到的对象。=> “结构”是应用编程的必须,而“解构”是底层计算的必须

赋值模板,不过是“变量名字”和“它的值”之间的位置关系的一个“说明”,这个说明是描述型的、声明风格的。因此它事实上在 JavaScript 语法解析阶段就完成了处理,根本不会“产生”任何运行期的执行过程。

[a,b]={a,b}: 左侧的“赋值模板”只是说明了一堆被声明的变量,也就是说,它们跟代码var x, y, z = 100中的x,y,z这样的名字声明没有任何差异,在处理上也是一样的。但是,这些赋值模板中声明的变量,每一个都“绑定”了一段赋值过程。这样的“赋值过程”在之前讲函数的非简单参数时也讲过,就是“初始器赋值”

undefined用于表达一个值 / 数据不存在,也就是“非值(non-value)”,例如 return 没有返回值,或变量声明了但没有绑定数据。

null用于表达一个对象不存在,也就是“非对象”,例如在原型继承中上溯原型链直到根类——根类没有父类,因此它的原型就指向null。

JavaScript 也就是 typeof() 所支持的 7 种类型,其中的“对象(object)”与“函数(function)”算一大类,合称为引用类型,而其他类型作为值类型。

从值 x 到引用,调用 Object(x) 函数。

从引用 x 到值,调用 x.valueOf() 方法;或调用 4 种值类型的包装类函数,例如 Number(x),或者 String(x)

obj = Object(x);

// 等效于(如果能操作内部槽的话)
obj.[[PrimitiveValue]] = x;

于是,当需要从对象中转换回来到值类型时,也就是把这个PrimitiveValue值取出来就可以了。而“取出这个值,并返回给用户代码”的方法,就称为valueOf()。

运算符很容易知道操作数的类型,例如“a - b”中的减号,我们一看就知道意图,是两个数值求差,所以 a 和 b 都应该是数值;又例如“obj.x”中的点号,我们一看也知道,是取对象 obj 的属性名字符串 x。

当需要引擎“推断目的”时,JavaScript 设定推断结果必然是三种基础值(boolean、number 和 string)。由于其中的 boolean 是通过查表来进行的,所以就只剩下了 number 和 string 类型需要“自动地、隐式地转换”。

JavaScript 会先调用ToPrimitive()内部操作来分别得到“a 和 b 两个操作数”可能的原始值类型。

如果x原本就是原始值,那么ToPrimitive(x)这个操作直接就返回x本身。

如果x是一个对象,且它有对应的五种PrimitiveValue内部槽之一,那么就直接返回这个内部槽中的原始值。由于这些对象的valueOf()就可以达成这个目的

如果一个运算无法确定类型,那么在类型转换前,它的运算数将被预设为 number。当预期是“number”时,valueOf()方法优先调用;否则就以toString()为优先.但x.valueOf()返回的是一个对象,那么就还会调用x.toString(),并最终得到一个字符串。

它们的左侧是一对大括号,而当它们作为语句执行的时候,会被优先解析成——块语句

{} + []
{} + {}
// 会被解析成块语句
{}; +[]
{}; +{}

因为严格地来讲,环境是 JavaScript 在语言系统中的静态组件,而上下文是它在执行系统中的动态组件

JavaScript 中,环境可以细分为四种,并由两个类别的基础环境组件构成。这四种环境是:全局(Global)、函数(Function)、模块(Module)和 Eval 环境;两个基础组件的类别分别是:声明环境(Declarative Environment)和对象环境(Object Environment)。

概念:所有的“环境”本质上只有一个功能,就是用来管理“名字 -> 数据”的对照表;

应用:“对象环境”只为全局环境的 global 对象,或with (obj)…语句中的对象obj创建,其他情况下创建的环境,都必然是“声明环境”。

如果一个任务只是任务,并没有执行,那么也就没有它的上下文;如果一个上下文从栈中撤出,那么就必须有地方能够保存这个上下文,否则可执行的信息就丢失了(这种情况并不常见);如果一个新上下文被“推入(push)”栈,那么旧的上下文就被挂起并压向栈底;如果当前活动上下文被“弹出(pop)”栈,那么处在栈底的旧上下文就被恢复了。

每一个执行上下文都需要关联到一个对照表。这个对照表,就称为“词法环境(Lexical Environment)

对于 JavaScript 来说,由于全局的特性就是“var 变量”和“词法变量”共用一个名字表,因此你声明了“var 变量”,那么就不能声明“同名的 let/const 变量”

环境在本质上是“作用域的映射”。作用域如果不需要被上下文管理,那么它(所对应的环境)也就不需要关联到上下文。

环境的确是在执行之前创建的,但比语法分析阶段要略晚。环境是“因为要执行,所以才创建的”。而语法分析,与执不执行并没有关系。当一个东西(例如全局的代码块)需要执行时,引擎才会创建它对应的环境

如果一个一般函数被调用,那么它也将形成一个对应的执行上下文,但是由于这个上下文是“被”调用而产生的,所以它会创建一个“调用者(caller)”函数的上下文的关联,并创建在 caller 之后。由于栈是后入先出的结构,因此总是立即执行这个“被调用者(callee)”函数的上下文

内部迭代过程

迭代的本质是多次函数调用,在 JavaScript 内部实现这一机制,本质上就是管理这些多次调用之间的关系。这显然包括一个循环过程,和至少一个循环控制变量。

迭代函数foo(),当你把它作为对象 x 的迭代器符号名属性,并通过对象 x 来调用它的迭代展开,事实上也就相当于只调用了多次的 return 语句。

// 迭代函数 function foo(x = 5) { return { next: () => { return {done: !x, value: x && x–}; } } } let obj={} obj[Symbol.iterator]=foo

console.log(…obj)//相当于只调用了 5 次 return 语句

学到了,对象可迭代属性有什么用,如何产生

yield 运算符的作用 逻辑上:它产生一次函数的退出,并接受下一次 tor.next() 调用所需要的进入; 数据上:它在退出时传出指定的值(结果),并在进入时携带传入的数据(参数)


在技术的历史长河中,虽然我们素未谋面,却已相识已久,很微妙也很知足。互联网让世界变得更小,你我之间更近。

在逝去的青葱岁月中,虽然我们未曾相遇,却共同经历着一样的情愫。谁的青春不曾迷茫或焦虑亦是无奈,谁不曾年少过

在未来的日子里,让我们共享好的文章,共同学习进步。有不错的文章记得分享给我,我不会写好的文章,所以我只能做一个搬运工

我叫 sunseekers(张敏) ,千千万万个张敏与你同在,18年电子商务专业毕业,毕业后在前端搬砖

如果喜欢我的话,恰巧我也喜欢你的话,让我们手拉手,肩并肩共同前行,相互学习,互相鼓励

文档信息

Search

    Table of Contents