ECMAScript 运算符怪谈 下

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]

以题开局

javascript 复制代码
let name = "let-name";
const animal = {
  name: "animal-Name",
  getName() {
    return this.name
  }
};
const log = console.log;
const getName = animal.getName;

log('getName():',getName());
log('animal.getName():', animal.getName());
log('(animal.getName)():', (animal.getName)());
log('(0, animal.getName)():', (0, animal.getName)());

这段代码在浏览器和nodejs环境中执行还能获得不一样的结果。

浏览器

window.name在没有显式赋值的情况下,那么结果如下:

javascript 复制代码
getName():              // 
animal.getName():       // animal-Name
(animal.getName)():     // animal-Name
(0, animal.getName)():  //

分析:

  • getName() 和 (0, person.getName)() 这两种方式执行, this是全局对象window
  • person.getName() 和 (person.getName)() 这种方式执行, this是 animal对象

nodejs环境

javascript 复制代码
getName():             // undefined
animal.getName():      // animal-Name
(animal.getName)():    // animal-Name
(0, animal.getName)(): // undefined

分析:

  • getName() 和 (0, animal.getName)() 这两种方式执行, this是 空对象 {}
  • animal.getName() 和 (animal.getName)() 这种方式执行, this是 animal对象

还要额外提醒一下,

  • window 对象真的有name属性,而且默认值是空字符串
javascript 复制代码
console.log('window.name:', window.name) // window.name:
  • 浏览器环境下,let和const申明的变量(申明环境记录),不会挂到全局对象window上,使用var或者直接变量赋值的才会挂在到window对象上(对象环境记录)。 这里主要为了解释浏览器环境下 getName() 和 (0, person.getName)() 这两种调用返回是 空字符串。
javascript 复制代码
var varName = 'varName';
let letName = 'letName';
xName = 'xName';

const log = console.log;
log(this.varName);  // varName
log(this.letName);  // undefined
log(this.xName);    // xName

这种诡异的行为,与之相关的有两个运算符:

  • ( ) 分组运算符
  • ( , ) 逗号运算符

接下来,来揭开面纱。 在此之前先回忆几个协议概念

引用记录(Reference Record

这个概念 在 协议的第六部分 6.2.5 The Reference Record Specification Type

这个 Reference Record Type 不是数据类型说的基本类型和引用类型中那个引用类型,是ECMA协议内部的一个概念。

引用记录类型用于解释诸如 delete、 typeof、赋值运算符、 super 关键字和其他语言特性等运算符的行为。

例如,赋值的左边操作数应该生成一个引用记录。

对标志符取值的时候,也会生成引用记录。

obj.xx 和 var 的x 本质都是标志符,其背后逻辑,是先获取引用记录,然后进行取值操作。

javascript 复制代码
const obj = {
  x: x
}
var x = 'x';
console.log(obj.x);
console.log(x);

细看一下Reference Record的结构。

Reference Record 里面有四个Filed,主要看 [[Base]], 其

就是编程中常见的值,比如 undefined, null , String , Boolean , Object等等

unresolvable,和他关联紧密有一个方法为 IsUnresolvableReference ( V ), 其主要就是判断引用可达不可以,其一种表现就是变量申明没有。

GetValue

其就是一个基本的取值操作,如果不是引用记录直接返回值,接下来就是对引用记录的取值操作,所以引用记录这个概念非常重要。

  1. 如果V不是引用记录,直接返回V

如下情况就是,细节到逗号运算符说

javascript 复制代码
(0,1) 
  1. 引用记录不可达, 报错
javascript 复制代码
console.log(xxxxxxxxxxxxxxxxx)   // Uncaught ReferenceError: a is not defined
  1. 如果是属性引用,进行取值。 其实其本质也就是检查 Reference Record[[Base]], 如果不是环境记录,如果不是 unresolvable,那么就是属性引用。

比如 obj.x

javascript 复制代码
const obj ={ x: 1 };
  1. 其他情况就是 环境记录

举个例子如下的 console.log(name)

javascript 复制代码
function logName(){
	var name = 'name';
  return log(){
      console.log(name);
  }
}

logName().log();

与 GetValue 对应的还有一个 PutValue, 相应的这个是设置值,了解即可。

分组(圆括号)运算符()

最常用来改变运算优先级。

javascript 复制代码
(1 + 2) * 3 = 9

额外说点有意思的, 如下的代码会抛出异常。

javascript 复制代码
const num = 1
(num).toString()
// ncaught ReferenceError: num is not defined

(num)会被解释为函数调用,解释为如下代码

javascript 复制代码
const num = 1(num).toString()

类似的代码,数组字面量,也会抛出异常。

javascript 复制代码
const num = 1
[1].toString()
// Uncaught TypeError: Cannot read properties of undefined (reading 'toString')

会被解释为如下代码

javascript 复制代码
const num = 1[1].toString()

废话那么多,只想说一下,每个语句最后添加 ;不是个坏习惯。

当然这些不是本节的重点,协议 13.2.9 The Grouping Operator 有一段备注。

翻译一下: 此算法不将 GetValue 应用于表达式计算。这样做的主要原因是,诸如 delete 和 typeof 之类的运算符可以应用于带括号的表达式,毕竟delete是可以用来删除引用关系的。

javascript 复制代码
const animal = {
  name: "animal-Name",
  getName(){
      return this.name
  }
};

delete (animal.getName)
console.log(animal.getName);       // undefined   getName被删除
console.log(Object.keys(animal));  // ['name']  

如果 (animal.getName)进行了取值, 上一节有阐述delete后面跟的是值的 (非引用记录),是直接返回 true 的,没有任何实质的操作,自然不能从 animal 上删除 getName属性

javascript 复制代码
"use strict"
delete true                            // true
delete function a(){}                  // true
console.log(`function a:`, a)          // Uncaught ReferenceError: a is not defined

这里拓展一下, delete 后面的 function a(){}属于函数申明,还是函数表达式呢?

答案是: 函数表达式。

可以逆向思维一下,如果是函数申明,delete执行完毕后,函数a应该是存在的。

javascript 复制代码
delete (function a (){}, 1)  
console.log("function a:", a)  // Uncaught ReferenceError: a is not defined

这是什么意思呢? 就是不会进行取值(GetValue)操作,而是拿着引用进行操作。

(animal.getName)()就等同拿着 animal.getName的引用直接进行操作。

javascript 复制代码
(animal.getName)()
// 等同于如下操作
animal.getName()     

到这里,如下的代码也就不难理解了。

javascript 复制代码
const animal = {
    name: "animal-Name",
    getName(){
        return this.name
    }
};
(animal.getName)();  // animal-Name

逗号运算符( , )

协议 13.16 Comma Operator ( , )

备注中说GetValue是必须被调用的, 啥意思,就是进行了取值操作。

javascript 复制代码
const animal = {
  name: "animal-Name",
  getName(){
      return this.name
  }
};
(0, animal.getName)()  // ''

上面调用代码可以如下伪代码

javascript 复制代码
 (0, const x = animal.getName)(); 

同理,如果这个时候进行delete ,那么是删除不了 getName 属性的。

javascript 复制代码
const animal = {
  name: "animal-Name",
  getName(){
    return this.name
  }
};

delete (0, animal.getName)
console.log("animal.getName:", animal.getName); // ƒ getName(){ return this.name  }

至此,解释了开局的题目, 此节完。

对比

对两段代码做个对比

javascript 复制代码
const animal = {
    name: "animal-Name",
    getName(){
        return this.name
    }
};
(animal.getName)();
javascript 复制代码
const animal = {
  name: "animal-Name",
  getName(){
     return this.name
  }
};
(0, animal.getName)() 

解析树对比

第一个语句是完全一致的,重点看第二个语句。

所以核心的操作,还是在于 CommaOperator 逗号运算符。

环境记录

本章节示例代码,当getName函数内部语句被执行时。

仅仅是有分组运算符()的图示:

  • this 的值,就是从函数环境记录取 ThisValue
  • thisValue 的值 是 animal 对象

逗号运算符(,)图示:

  • this 的值,就是从函数环境记录取 ThisValue
  • thisValue 的值 是 全局对象。

不知道大家眼中函数调用的 this 是什么样的?

依照协议描述:

  • 函数调用时,在真正执行函数体的语句前,会依据函数的 [[ThisMode]]即this的模式,先获取 this的值。函数调用一定有this, 箭头函数的this ,是从外面借来的。
  • 然后把 this 的值,绑定到函数环境记录,
  • 函数体的语句执行时,用到this,再从 函数环境记录中 取 this 的值。

更多细节,后面的 函数this之路有详细的解答。

相关推荐
会飞的鱼先生15 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇30 分钟前
一文搞定CSS Grid布局
前端
0xHashlet35 分钟前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝36 分钟前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大38 分钟前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂39 分钟前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端
凌叁儿39 分钟前
从零开始搭建Django博客③--前端界面实现
前端·python·django
博弈美业系统Java源码39 分钟前
连锁美业管理系统「数据分析」的重要作用分析︳博弈美业系统疗愈系统分享
java·大数据·前端·后端·创业创新
木子李i40 分钟前
Cesium离线使用和部署地图影像
前端·cesium
本本啊42 分钟前
node 启动本地应用程序并设置窗口大小和屏幕显示位置
前端·node.js