ECMAScript 运算符怪谈 上

前言

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

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

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

热身准备

引用记录(Reference Record

这个概念已经提了很多次了,本章节只要重点关注其[[Base]]字段, 有个特殊的值就是 unresolvable,和他关联紧密有一个方法为 IsUnresolvableReference ( V ), 其主要就是判断引用可达不可以,其一种表现就是变量申明没有。

一元+

arduino 复制代码
javascript
复制代码
+10n // caught TypeError: Cannot convert a BigInt value to a number
-10n // -10n

如下是协议对比图:

typeof 运算符

typeof有四怪:

  1. typeof null
  2. typeof NaN
  3. typeof Function.prototype
  4. typeof document.all

一起打怪

typeof的安全性

typeof 对未申明的变量进行运算时,其返回 "undefined",而不是报错。其相对 instanceof等一些运算符来说,是相对安全的。

csharp 复制代码
javascript
复制代码
typeof 1                  // "number"
typeof undeclaredVariable // "undefined"

那么,本小节内容完! 哈哈,开个玩笑,一起来知其所以然。

看ECMAScript 13.5.3 The typeof Operator,截个图,方便看

上图标注了三个地方, 结合前面提到的引用记录的知识,就是如果引用记录不可达,就直接返回 undefined

到此为止,就解释了typeof未申请变量时,返回值 undefined

在typeof的判断逻辑中,还能获得一个额外的信息,

functionobject本是同根生,只不是是挂着 [[Call]]标识罢了。

typeof 暂时性死区

ES6加入了块级作用域的 letconst之后,typeof 就变得不是绝对安全了。

在被申明之前使用 typeof 会抛出 ReferenceError。这就是熟知的暂时性死区。

csharp 复制代码
javascript
复制代码

typeof letVariable; // ReferenceError
typeof constVariable; // ReferenceError

let letVariable;
const constVariable = "hello";

当然, class 是自带这个特性的,未申明之前,使用typeof也会报错。

vbnet 复制代码
javascript
复制代码
typeof MyClass; // ReferenceError

class MyClass{}

这里可能大家可能会有疑问,你前面刚刚提到了,引用不可达,返回的是 undefined啊,也就是压根没有这个标志符。

但是 本示例就不一样,这个标志符是存在的,当执行 typeof letVariable之时, letVariable已经实例化,只是没有初始化。

typeof 是对值进行操作,发生了 取值GetValue 行为,本示例 typeof letVariable:

  1. 评估 letVariable , 返回引用记录
  2. 对引用记录取值
  3. 对值进行判断,并返回类型值

取值行为过程会进行判断,如果申明未初始化,就会抛出 ReferenceError

更多细节在 《一些代码背后的逻辑》。

所以:

typeof undeclaredVariable: 检查引用记录不可达后,直接返回 undefined ,这个行为没有发生取值行为。

typeof letVariable: 对引用记录进行取值,取值时发现 词法申明未初始化,就抛出了 ReferrenceError

typeof null

ini 复制代码
javascript
复制代码
typeof null === "object";

这个问题的产生可以追溯到JavaScript的第一个版本,该版本,单个值在栈中占用32位的存储单元。

32又分为两部分:

标记位(三位): 表示数据类型

其他位: 表示数据

五种数据类型:

000: object

001: integer

010: double

100: string

110: boolean

null是个特别的家伙:

而js 里的null 是机器码NULL空指针(0x00), 第0位到第31位皆为0, 自然标记位是 000, 所以返回的值为 object。

在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型, null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回 "object"。

当然现在的V8引擎不是这么判断数据类型的,为了历史包袱,对null对了特殊处理。

更多详情可以参见 The history of "typeof null"

typeof NaN

NaN 明明说挂牌说自己不是一个数字,为啥 typeof 的结果 是number呢? 哈哈,其实可以反问,不返回 number那返回啥呢?

这个就不得不提 IEEE 754标准 了, 准定义了如何以二进制形式表示实数,包括单精度(32位)、双精度(64位)和扩展精度(80位)。

而 NaN 真的是按照 IEEE 754 标准格式存储的值,所以嘛, 是 number ,合情合理,接下来再简单介绍一下。

以双精度浮点数(64位)为例,这64位 又分为 符号位, 指数位,和尾数位(图中的有效数字位)。

数值中有 无穷大(+Infinity、-Infinity)、NaN(Not a Number,非数值)等特殊值,特殊值有特殊的表达方式。

指数位全部为1,11111111111(十进制2047)

  • 52位尾数部分全为0时,

    • 若符号位是0,则表示+Infinity(正无穷),
    • 若符号位是1,则表示-Infinity(负无穷)
  • 52位尾数部分不全为0时,表示NaN(Not a Number)

可以借用 IEEE 754 浮点数 - 在线工具去查看NaN的二进制64位的值

  1. 符号位 0
  2. 指数位全为 1
  3. 小数部分全为 0

+Infinity

-Infinity

更多细节参见 《再谈数值》。

typeof Function.prototype

javascript 复制代码
javascript
复制代码
typeof Function.prototype            // 'function'
typeof Boolean.prototype             //  'object'
typeof String.prototype              //  'object

是不是觉得奇怪, 实际上看看协议 Function.prototype返回的是函数对象 ,即开发者眼中的 function ,那么一地那就不奇怪了。

最后一行:函数原型对象被指定为一个函数对象,以确保与在ECMAScript 2015规范之前创建的ECMAScript代码兼容。

同理呢

javascript 复制代码
javascript
复制代码
Object.prototype.toString.call(Function.prototype)    // [object Function]

typeof document.all

在浏览器中有一个属性特别好用,也特别的奇葩。 她就是document.all, 其返回页面所有的元素(nodeType = 1), 注意不是节点。

至于什么是元素,什么是节点,可自行查看 Node.nodeType

但是在高版本的IE和其他浏览器中 typeof document.all 返回的却是 undefined, 是不是有点搞笑。

长话短说, IE浏览器发明了document.all, 非常受欢迎, 开发者们以是否有 document.all 来判断是不是IE浏览器,其他浏览器厂商觉得很香, 也开发了这个属性。 但是为了不影响既有的判断逻辑,于是就有了这幽默的现状。

javascript 复制代码
javascript
复制代码
!document.all             // true
!! document.all           // false
typeof document.all       // 'undefined'
document.all == undefined // true

这个是怎么做到的呢? 协议内部有一个叫做 The [[IsHTMLDDA]] Internal Slot 的玩意,简单理解就是内部属性或者标记。 有这个标记的对象,他的行为表现的像 undefined

  • 在转为布尔值的时,
  • 宽松比较的时
  • typeof 运算时

具体的协议内容,截图奉上。

额外说说吧

其实document还有其他类似的很好用的属性。

属性 说明
document.anchors 有 name 属性的 a 元素
document.images 所有图片
document.forms 所有表单
document.links 具有 href 属性值的 元素与 元素的集合
document.scripts script元素集合

小结

  1. typeof 可以判断基本类型,但是存在 typeof null的历史 "诟病", 配合 Object.prototype.toString 使用效果更加
  2. typeof 相对安全,在ES6出现块级组作用域之后,出现了暂时性死区,情况发生了变化
  3. typeof null 会返回 object, 是历史遗留问题
  4. document.all 在转为布尔值,宽松比较,typeof 运算时,表现得像 undefined, "罪魁祸首" 是浏览器战争。

delete 操作符

delete 你在删除什么?

MDN delete 操作符 的解释是: 用于删除对象的某个属性。

arduino 复制代码
javascript
复制代码
const person = {"name":"程序员", age:10};
delete person.age  
console.log('age' in person);   // false

这样一看,似乎很河狸,但是 delete 操作符的右边可不仅仅能跟随对象的属性, 还可以是数字字面量和对象字面量等。

go 复制代码
javascript
复制代码
var x = 10;
delete x;                  // false
delete 10;                 // true
delete {};                 // true
delete undeclaredVariable  // true

看到结果是不是有点小惊讶,delete x ,换一种写法就很好懂 (假设是浏览器环境)delete window.x, 相当于从全部对象中删除 x属性,可惜的是返回的是 false。

遇事不决,协议科学,协议怎么描述的 delete UnaryExpression

至于 Reference Record 在typeof 已经提到过了, 赋值行为可以生成引用记录,字面量就没那么幸运了。

标记1: 如果不是引用记录,直接返回true。 这解释 delete 10 delete {} 为什么返回true

标记2: 如果引用不可达,直接返回true。这解释 delete undeclaredVariable 删除未申明的变量返回true。

协议的第4步骤和第5步骤分别对应如下的情况,自己领会领会,比较好懂 ,一个是属性引用,一个实际上是从环境记录上删除绑定关系,当然,归根到底,都是要从具体的某个对象进行删除操作。

javascript 复制代码
javascript
复制代码
delete window.name    // 第4步对应的场景
delete name           // 第5步对应的场景

接下来讨论一下返回值的问题,下面的例子: delete a返回 true,可是压根就没删除掉啊。

arduino 复制代码
javascript
复制代码
{
  const a = 1;
  delete a       // true
  console.log(a) // 1
} 

到这里你可以会有点疑问,为啥返回 true呢? 那这里true代表什么含义呢? 难道不是成功嘛?

delete 返回值

delete 的返回值,从上面其实已经得知,是布尔值。

其值的真实含义就是 是删除行为没有异常

deleteECMA-262, 1st edition (June 1997)ECMAScript 的第1版出现的 ,具体页码是51页 ,这时候还没有 try catch语法, 其还待在 保留关键字区域,具体页码23页

try catch是在 ECMA-262, 3rd edition ( December 1999 )ECMAScript 的第3版 才出现的, 具体页码 82页

除此之外,在 ECMAScript协议的 Introduction 里面有清晰的提到,try catch 是 第三版本引入的。

或者看看ES1到ES5的演进图

没有try catch就没法捕获错误,用true表示没有异常,false删除失败,这样就显得很合理了。

那么我们来总结一下:

  1. 返回true, 不一定删除成功,只能说没有发生异常
  2. 返回false,一定没有删除成功

接着重点讨论,

  1. 什么情况下 delete 返回true,但是删除不成功
  2. 什么情况 delete 返回false

delete 返回值false的情况

先简单回顾一下两个概念

不可配置

不得不提数据属性和访问器属性,更多详情可以查看JavaScript高级编程或者自行搜索。

二者都有的属性

  • configurable

当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false

  • enumerable

当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false

非自身属性

当使用object.x时, 这个x可能来自对象本身,也可能来自原型链。 来自对象本身的属性就是自身属性。 判断是不是自身属性可以通过 Object.prototype.hasOwnProperty 来判断。

javascript 复制代码
javascript
复制代码
var obj = {x: "x"};
Object.prototype.hasOwnProperty.call(obj, "x")          // true
Object.prototype.hasOwnProperty.call(obj, "toString");  // false

返回false的情况就是,如果这个属性是

  1. 自身的 属性
  2. 不可配置 属性
  3. 其他情况

哦,懂了,懂了,那就一起看两个例子:

javascript 复制代码
javascript
复制代码
var x = 10;
delete x;           // false
delete null;        // true
delete undefined;   // false

会不会感到有点惊喜呢? 挨个简单解释一下:

  • delete x

var申明的变量是全局变量,在浏览器中,等同于window.x, 这种申明的属性,数据属性configurable值为false。

javascript 复制代码
javascript
复制代码
var x = 10;
Object.getOwnPropertyDescriptor(window, 'x')
  • delete null

null是关键字,这里返回true表示没有异常。

  • delete undefined

undefined在实现上,其实本质是一个全局属性

delete 返回true,但是没删除成功的常见情况

  1. 删除不存在的属性
javascript 复制代码
javascript
复制代码
delete window.CCCCCCCCCCCCCCCCCCCCCC   // true
  1. 任何用letconst声明的属性不能够从它被声明的作用域中删除
ini 复制代码
javascript
复制代码
{
  let a = 1;
  let b = 2;
  delete a; 
  delete b;
  console.log(a, b);  // 1  2
}
  1. delete null 4. 原型上的属性
javascript 复制代码
javascript
复制代码
Object.prototype.cccccccccccccccccccccccccc  = 1;
delete Object.prototype.cccccccccccccccccccccccccc  // true
  1. 其他情况

嗯,其他情况

void

语法 void expression

两层含义:

  1. 执行后面的表达式 expression
  2. 返回 undefined

至于作用吗,慢慢细说。

undefined 被改写

下面代码尝试修改 undefined, 会发现修改失败。

原理很简单,之前也提到过, undefined 是以变量的形式 挂载全局对象 globalThis上(浏览器环境是window), 并且不可以被改写和配置。

javascript 复制代码
javascript
复制代码
// 全局
undefined = 10;
console.log('global undefined:', undefined, Object.getOwnPropertyDescriptor(globalThis, 'undefined'));
// global undefined: undefined {value: undefined, writable: false, enumerable: false, configurable: false}

低版本浏览器,譬如IE8就没这么幸运了。

既然undefined 是变量的形式实现的,而不是关键字的方式实现的,那么她就存在被改写的风险。如下两个例子,就证明了, undefined可能并不是你期待的的那个 undefined

javascript 复制代码
javascript
复制代码
{
    let undefined = 20;
    console.log('undefined:', undefined) // undefined: 20
}
javascript 复制代码
javascript
复制代码
function log(undefined){
    undefined = 30;
    console.log('undefined:', undefined);  // undefined: 30
}
log();

一个不小心出现如下代码就尴尬了。

ini 复制代码
javascript
复制代码
;(function (){
	let  undefined = 100;

	// 此处省略一亿行代码

  const val = await someFun();

  if(val == undefined){
    alert('出错了');
  }
  
  
})()

这个时候, void的优势就出现了, 定义一个方法,void其后面你是写0,还是1,还是啥其实并不重要。

csharp 复制代码
javascript
复制代码
function getUndefined(){
  return void 0;
}

这个思路在 undescore 这个库里面也得到了很好的应用 isUndefined

javascript URI

比如如下你使用了 a标签,但是点击后不想有任何操作,就可以用到void(0) , 当然你也可以做点啥,比如第二个例子刷新页面。

css 复制代码
html
复制代码
<a href="javascript:void(0);">
   啥都没干
</a>

<a href="javascript:void(location.reload());">
  刷新页面
</a>

href="javascript:void(0);"这里其实还有另外一层含义,

就是阻止了 a标签的默认行为,因为 onclick 的响应是优先于 href 的跳转的。

下面的两份代码是等同的

xml 复制代码
html
复制代码
<!--href 和 onclick -->
  <a href="https://www.baidu.com" onclick="onClick1(event)">
      href + onclick
  </a>
  <script>
      function onClick1(ev) {
          console.log("clicked");
          ev.preventDefault();
      }
  </script>
xml 复制代码
html
复制代码
<!--href void 和 onclick -->
<a href="javascript:void(0);" onclick="onClick2(event)">
    href + void + onclick
</a>
<script>
    function onClick2(ev) {
        console.log("clicked");
    }
</script>

还有如果你使用 typescript, 在编写代码的时候,如果写了 a 标签,没有写 href属性,也会报警告或者爆红的。这个时候 也是 可以请 javascript:void(0),当然也可以修改 eslint 的规则。

立即执行函数表达式(IIFE)

void 后面跟踪是表达式, 会先让后面的代码执行。 其比起常用的方式省略一堆括符。

javascript 复制代码
javascript
复制代码
// 常用
;(function () {
  console.log("runned!");
})();

// void
;void function () {
  console.log("runned!");
}();

// "runned!"

关系运算符:< > <= >=

先看段代码

less 复制代码
javascript
复制代码
({}) < ({})         // fasle
({}) > ({})         // false
({}) <= ({})        // true
({}) >= ({})        // true

此时,你看到结果是不是有点晕,其实也没那么复杂。

对象自然是不能比较关系的,肯定是需要转为基本数据类型(原始值)的,原始值比较,大概又分两类

  • 字符串
  • 数值

字符串

字符串比较,本质是依次比较(UTF-16格式)字符对应的码元(编码单元)的数值大小,可以通过charCodeAt获取对应码元的数值。

arduino 复制代码
javascript
复制代码
"a" > "b"  // false
"a".charCodeAt(0)   // 97
"b".charCodeAt(0)   // 98

当然UTF-16格式编码的字符,有的字符需要两个码元来表示一个字符,比如 𢂘

\uD848是一个码元的数值,\uDC98是另外一个码元的数值

arduino 复制代码
javascript
复制代码
'\uD848\uDC98' == '𢂘'  // true
'𢂘'.charCodeAt(0)     // 55368 ,其等于16进制的 D848
'𢂘'.charCodeAt(1)     // 56472 ,其等于16进制的 DC98

其实和字符相关的还有数值,叫做码点。这个码点是Unicode字符集里面规定的,你简单理解为一个唯一编号,你可以通过codePointAt方法获得其码点的值。

arduino 复制代码
javascriptjavascript
复制代码
'𢂘'.codePointAt(0)   // 139416

这里就给大家留一个问题, 字符串比较,比较的是码元的值还是码点的值呢?

数值

数值这里又分几种值

  • 正无穷, 负无穷
  • BigInt
  • NaN
  • 普通的Number值

处理各种特殊情况之后,就是正常的比较了。

关系运算符内部实现

协议更多细节可查看 Relational Operators

<=为例

  1. 取值

取左右操作数的值,调用内部的 IsLessThan ( x, y, LeftFirst ) 进行比较。返回值如果是 undefined或者是 true返回false

注意,这里调用 IsLessThan(rval, lval, false),第一参数传入的是右操作数,第二个传入的是左操作数。

  1. 核心 IsLessThan ( x, y, LeftFirst )

基本步骤

  • 取原始值

  • 如果都是字符串,依次比较码元的值

  • 否则

    • 如果 一边是BigInt一边是字符串,字符串转为BigInt,

      • 如果返回undefined , 返回undefined
      • 返回 BigInt比较的值
    • 左右两边转为数值, Number, BigInt 或者NaN

    • 如果左右两边类型相同

      • 是Number,返回比较值
      • 是BigInt, 返回比较值
    • 如果一边是NaN, 返回 undefined

    • 如果左操作数是负无穷, 右边是正无穷,返回true

    • 果果左操作数是正无穷, 右边是负无穷,返回false

    • 返回左右两个操作数的比较值

核心又变成了取原始值 ToPrimitive(x, number),方法定义为ToPrimitive ( input [ , preferredType ] ),preferredType 参数为 number。

别看上面第一步写了那么多,其就是调用定义的Symbol.toPrimitive方法。 原值转为不是本节的重点,你只需要知道 object的实例和原型上默认是没有这个方法的, 就会走后面的逻辑。

javascript 复制代码
javascript
复制代码
 Object.prototype[Symbol.toPrimitive] ;  // undefined
({})[Symbol.toPrimitive]                 // undefined

所以继续跟踪OrdinaryToPrimitive(input, preferredType)方法, 协议定义为 OrdinaryToPrimitive ( O, hint )协议中preferredType的值为 number

传入的是 number, 所以methodNames 的集合是 [valueOf, toString]

scss 复制代码
javascript
复制代码
var obj = ({});
obj.valueOf()    // {}
obj.toString()   // '[object Object]'

valueOf 返回的值对象,所以 obj的原值为 字符串 [object Object]

原值已经获得,回归 IsLessThan ( x, y, LeftFirst ) 的步骤(省略后续)

  • 取原始值
  • 如果都是字符串,依次比较码元的值

直接依次较码元即可。因为两边的值都是[object Object], 具体看比较逻辑如下图

长度相同, lx<ly不成立,所以 IsLessThan(rval, lval, false) 返回了 false。

这里出了结果,就可以来最后判断 ({}) <= ({})的返回值了,

看上图的协议描述,因为 IsLessThan(rval, lval, false ) 返回值是 false, 走 Otherwise的逻辑,就是返回 true.

所以, 哦豁。

less 复制代码
javascript
复制代码
({}) <= ({})   // true

整体看一下, 不难发现 < > <= >=都依赖了 isLessThan,再细细思考一下,

场景罗列

IsLessThan ( x, y, LeftFirst )的第三个参数LeftFirst ,表示是不是使用第一个参数作为比较的左操作数。 比如

  • IsLessThan ( x, y, true ), LeftFirst 为 true,x 在左边
    其内部 类似 原值x < 原值y ? true: false
  • IsLessThan ( x, y, false),LeftFirst 为 false,y 在左边
    其内部 类似 原值y < 原值x ? true: false

对四种比较 < > <= >= 运算逻辑汇总

比较关系 IsLessThan参数 IsLessThan 返回值 r 最终值
lVal < rVal IsLessThan(lVal, rVal, true) 特殊情况返回undefined 原值lVal < 原值rVal ? true: false r === undefined ? false: true
lVal > rVal IsLessThan(rVal, lVal, false) 殊情况返回undefined 原值lVal < 原值rVal ? true: false r === undefined ? false: true
lVal <= rVal IsLessThan(rVal, lVal, false) 殊情况返回undefined 原值lVal < 原值rVal ? true: false r === undefined r === true ? false: true
lVal >= rVal IsLessThan(lVal, rVal, true) 殊情况返回undefined 原值lVal < 原值rVal ? true: false r === undefined r === true ? false: true

从表格可以得出

  • 四种运算在在 IsLessThan 中的计算,是一致的,都是

    • 殊情况返回 undefined
    • 原值lVal < 原值rVal ? true: false
  • 只是外围的判断不一样而已

    • r = IsLessThan
    • 含有等于时 r === undefined || r === true ? false: true 没有小于,就是小于等于或者大于等于
    • 否则 r === undefined ? false: true

有点绕,是不,根本看不懂,其实简单记忆 不小于,反之就是大于等于或者小于等于

小结

  1. ({}) <= ({}) 取原值后比较,最终等同于 ('[object Object]') <= ('[object Object]')
  2. 不小于,反之就是大于等于或者小于等于 的原则,最终是 true
less 复制代码
javascript
复制代码
// ({}) 转为原值为  `[object Object]` 
({}) < ({})         // fasle
({}) > ({})         // false
({}) <= ({})        // 不小于就是大于等于或者小于等于,true
({}) >= ({})        // 不小于就是大于等于或者小于等于,tru
相关推荐
layman052812 分钟前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝13 分钟前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML13 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia31114 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生29 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇44 分钟前
一文搞定CSS Grid布局
前端
0xHashlet1 小时前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝1 小时前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大1 小时前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂1 小时前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端