null means "no object", undefined =>"no value". Really it's an abstraction leak: null and objects shared a Mocha type tag.
下面列出各种数据类型 typeof 对应的结果:
Operand
Result
undefinded
"undefined"
null
"object"
Boolean value
"boolean"
Number value
"number"
BigInt value (ES11)
"bigint"
String value
"string"
Symbol value (ES6)
"symbol"
宿主对象(由 JS 环境提供)
取决于具体实现
Function
"function"
All other values
"object"
typeof returning "object" for null is a bug. It can’t be fixed, because that would break existing code. Note that a function is also an object, but typeof makes a distinction. Arrays, on the other hand, are considered objects by it.
function myObjectIs (x, y) {
if (x === y) {
// x === 0 => compare via infinity trick
return x !== 0 || (1 / x === 1 / y)
}
// x !== y => return true only if both x and y are NaN
return x !== x && y !== y
}
function test1() {
var a = 11
eval('(a = 22)')
console.log(a) // 22
}
function test2() {
var a = 11
new Function('return (a = 22)')()
console.log(a) // 11
}
直接调用和间接调用最大的区别在于他们的作用域不同:javascript function test() { var x = 2, y = 4 console.log(eval("x + y")) // Direct call, uses local scope, result is 6 var geval = eval; console.log(eval("x + y")) // Indirect call, uses global scope, throws ReferenceError becausexis undefined }
间接调用 eval 最大的用处(可能也是唯一的实际用处)是在任意地方获取到全局对象(然而 Function('return this')() 也能做到这一点): javascript // 即使是在严格模式下也能起作用 var global = ("indirect", eval)("this");
今天来聊一聊 JavaScript 中让人摸不着头的设计失误。
Brendan Eich 在 1995 年加入 Netscape 公司,当时 Netscape 和 Sun 合作开发一个可运行在浏览器上的编程语言,当时 JavaScript 的开发代号是 Mocha。Brendan Eich 花了 10 天完成了第一版的 JavaScript。
由于设计时间太短,语言的一些细节考虑得不够严谨,一些因不可抗因素而无法修复的 bug,加之后来填坑过程中新挖的坑,总之开发者表示很烦...
一起看下 JavaScript 设计的「坑」有哪些?
一、typeof null === 'object'
这是一个众所周知的失误。
对于刚接触 JavaScript 的朋友,有可能会直觉性地、错误地认为
typeof null === 'null'
,这是不对的。在 JavaScript 中,数据类型在底层都是以二进制形式表示的。在初版 JavaScript 中,以 32 位为单位存储一个值,其中包括一个类型标记(1-3 位)和该值的实际数据。类型标记存储在单元的低位。其中有五个:
000
:object,数据是一个对象的引用。1
:int,数据是一个 31 位有符号整数。010
:double,数据是一个双精度浮点数。100
:string,数据是一个字符串110
:boolean,数据是一个布尔值。也就是说,最低位如果是 1,那么类型标记长度只有 1 位;如果最低位是 0,那么类型标记长度为 3 位,为四种类型提供两个附加位。
有两个特殊的值:
undefined
(JSVAL_VOID)是整数 −2^30^(整数范围之外的数字)null
(JSVAL_NULL)是机器码空指针。或:一个对象类型标记加上一个零的引用。(null
二进制表示全是 0)现在我们知道为什么
typeof
会认为null
是一个对象了,它检查了null
的类型标记,且类型标记表示object
。以下是该引擎的typeof
代码。上面的代码执行的步骤是:
v
是否undefined
(VOID)。通过==
比较值是否相同:[[Class]]
将其标记为一个函数(4),则v
是一个函数。否则,它是一个对象。这是由typeof null
产生的结果。null
检查,可以由以下 C 宏执行。这似乎是一个非常明显的错误,但请不要忘记,只有很少的时间来完成 JavaScript 的第一个版本。
下面列出各种数据类型
typeof
对应的结果:某文章表示:
在 JavaScript V8 引擎中,针对
typeof null === 'object'
这种“不规范”情况,对null
提前做了一层判断。假设在 V8 中把这行代码删掉,typeof null
会返回undefined
。好了,关于
typeof null === 'object'
的话题告一段落。二、typeof NaN === 'number'
不确定这个算不算一个设计失误,但毫无疑问这是反直觉的。
三、NaN、isNaN()、Number.isNaN()
在 JavaScript 中,NaN 是一个看起来很莫名其妙的存在。当然 NaN 不是只有 JavaScript 才存在的。其他语言也是有的。
1. NaN
NaN
是一个全局对象属性,其属性的初始值就是NaN
,和Number.NaN
的值一样。NaN
是 JavaScript 中唯一一个不等于自身的值。虽然这个设计其实理由很充分(参照前面推荐的那个 Slide,在 IEEE 754 规范中有非常多的二进制序列都可以被当做NaN
,所以任意计算出两个NaN
,它们在二进制表示上很可能不同),但不管怎样,这个还是非常值得吐槽...2. isNaN()
isNaN()
是全局对象提供的一个方法,它的命名和行为非常让人费解:NaN
,因为所有对于所有非数字类型的值它也返回true
;NaN
的类型是number
,应当被认为是一个数值。isNaN()
方法,当参数值是NaN
或者将参数转换为数字的结果为NaN
,则返回true
,否则返回false
。因此,它不能用来判断是否严格等于NaN
。3. Number.isNaN()
ES6 提供了
Number.isNaN()
方法,用于判断一个值是否严格等于NaN
,终于是拨乱反正了。和全局函数
isNaN()
相比,Number.isNaN()
不会自行将参数转换成数组,它会先判断参数是否为数字类型,如不是数字类型则直接返回false
,接着判断参数值是否为NaN
,若是则返回true
。4. 总结几种判断值是否为 NaN 的方法
四、==、=== 与 Object.is()
JavaScript 是一种弱类型语言,存在隐式类型转换。因此,
==
的行为非常令人费解。所以,各种 JavaScript 书籍都推荐使用
===
替代==
(仅在 null checking 之类的情况除外)。但事实上,
===
也并不总是靠谱,它至少存在两类例外情况。(Stricter equality in JavaScript)直到 ES6 才有一个可以比较两个值是否严格相等的方法:
Object.is()
,它对于===
的这两者例外都做了正确的处理。如果 ES6 以下,这样实现
Object.is()
:五、分号自动插入机制(ASI)
1. Restricted Productions
据 Brendan Eich 称,JavaScript 最初被设计出来时,上级要求这个语言的语法必须像 Java。所以跟 Java 一样,JavaScript 的语句在解析时,是需要分号分隔的。但是后来出于降低学习成本,或者提高语言的容错性的考虑,他在语法解析中加入了分号自动插入的纠正机制。
这个做法的本意当然是好的,有不少其他语言也是这么处理的(比如 Swift)。但是问题在于,JavaScript 的语法设计得不够安全,导致 ASI 有不少特殊情况无法处理到,在某些情况下会错误地加上分号(在标准文档里这些被称为 Restricted Productions)。
最典型的是
return
语句:这导致了 JavaScript 社区写代码时花括号都不换行,这在其他编程语言社区是无法想象的。
2. 漏加分号的问题
有好几种情况要注意(更多 ASI 详情看上面推荐的文章),比如:
3. semicolon-less
由于以上这些已经是语言特性了,并且无法绕开,无论怎样我们都需要去学习掌握。
六、Falsy values
在 JavaScript 中至少有七种假值(在条件表达式中与
false
等价):0
、0n
、null
、undefined
、false
、''
以及NaN
。(其中0n
是 BigInt 类型的值)七、+、- 操作符相关的隐式类型转换
大致可以这样记:作为二元操作符的
+
会尽可能地把两边的值转为字符串,而-
和作为一元操作符的+
则会尽可能地把值转为数字。八、null、undefined 以及数组的 “holes”
在一个语言中同时有
null
和undefined
两个表示空值的原生类型,乍看起来很难理解,不过这里有一些讨论可以一看:不过数组里的 "holes" 就非常难以理解了。
产生 holes 的方法有两种:一是定义数组字面量时写两个连续的逗号:
var a = [1, , 2]
;二是使用Array
对象的构造器:new Array(3)
。数组的各种方法对于 holes 的处理非常非常非常不一致,有的会跳过(
forEach
),有的不处理但是保留(map
),有的会消除掉 holes(filter
),还有的会当成undefined
来处理(join
)。这可以说是 JavaScript 中最大的坑之一,不看文档很难自己理清楚。具体可以参考这两篇文章: Array iteration and holes in JavaScript ECMAScript 6: holes in Arrays
九、 Array-like objects
在 JavaScript 中,类数组但不是数组的对象不少,这类对象往往有
length
属性、可以被遍历,但缺乏一些数组原型上的方法,用起来非常不便。比如在为了能让arguments
对象用上Array.prototype.shift()
方法,我们往往需要先写这样一条语句,非常不便。在 ES6 中,arguments 对象不再被建议使用,我们可以用 Rest parameters(
const fn = (...args) => {}
),这样拿到的对象(args
)就直接是数组了。不过在语言标准之外,DOM 标准中也定义了不少 Array-like 的对象,比如 NodeList 和 HTMLCollection。对于这些对象,在 ES6 中我们可以用 spread operator 处理:
arguments
在非严格模式下(sloppy mode)下,对 argument 赋值会改变对应的形参。
十、函数作用域与变量提升(Variable hoisting)
函数作用域
蝴蝶书上的例子想必大家都看过:
函数级作用域本身没有问题,但是如果只能使用函数级作用域的话,在很多代码中它会显得非常反直觉。比如上面的这个循环例子,对于程序员来说,根据花括号的违章确定变量作用域远比找到外层函数容易得多。
在以前,要解决这个问题,我们只能使用闭包 + IIFE 产生一个新作用域,代码非常难看(其实
with
以及catch
语句后面跟的代码块也算是块级作用域,但这并不通用)。幸而现在 ES2015 引入了
let
/const
,让我们终于可以用上真正的块级作用域。变量提升
JavaScript 引擎在执行代码的时候,会先处理作用域内所有的变量声明,给变量分配空间(在标准里叫 binding),然后在再执行代码。
这本来没什么问题,但是
var
声明在被分配空间的同时也会被初始化成undefined
(ES5 中的 CreateMutableBinding),这就相当于把 var 声明的变量提升到了函数作用域的开头,也就是所谓的 “hoisting”。ES6 中引入的
let
、const
则实现了 temporal dead zone,虽然进入作用域时用let
和const
声明的变量也会被分配空间,但不会被初始化。在初始化语句之前,如果出现对变量的引用,会抛出ReferenceError
错误。在标准层面,这是通过把 CreateMutableBing 内部方法分拆成 CreateMutableBinding 和 InitializeBinding 两步实现的,只有 VarDeclaredNames 才会执行 InitializeBinding 方法。
let、const
然而,
let
和const
的引入也带来了一个坑。主要是这两个关键词的命名不够精确合理。const
关键词所定义的是一个 immutable binding(类似于 Java 的final
关键词),而非真正的常量(constant),这一点对于很多人来说也是反直觉的。ES6 规范的主笔 Allen Wirfs-Brock 在 ESDiscuss 的一个帖子里表示,如果可以从头再来的话,他会更倾向于选择
let var
/let
或者mut
/let
替代现在的这两个关键词,可惜这只能是一个美好的空想了。for...in
for...in
的问题在于它会遍历到原型链上的属性,这个大家应该都知道的,使用时需要加上obj.hasOwnProperty(key)
判断才安全。在 ES6+ 中,使用
for(const key of Object.keys(obj))
或者for(const [key, value] of Object.entries())
可以绕开这个问题。with
最主要的问题在于它依赖运行时语义,影响优化。
此外还会降低程序可读性、易出错、易泄露全局变量。
eval
eval
的问题不在于可以动态执行代码,这种能力无论如何也不能算是语言的缺陷。Scope
它的第一个坑在于传给 eval 作为参数的代码段能够接触到当前语句所在的闭包。
而用
new Function
动态执行的代码就不会有这个问题。因为new Function
所生成的函数是确保执行在最外层作用域下的(严格来说标准里不是这样定义的,但实际效果基本可以看作等同,除了new Function
中可以获取到arguments
对象)。直接调用 vs 间接调用(Direct Call vs Indirect Call)
第二个坑是直接调用
eval
和间接调用的区别。事实上,但是「直接调用」的概念就足以让人迷糊了。
首先,
eval
是全局对象上的一个成员函数;但是,
window.eval()
这样的调用 不算是 直接调用,因为这个调用的 base 是全局对象而不是一个 "environment record"。接下来的就是历史问题了。
eval
调用并没有直接和间接的区分;eval
使用方式是 直接调用,如果eval
被间接调用了或者被赋值给其他变量了,JavaScript 引擎 可以选择 报一个 Runtime Error(ECMA-262 2nd Edition, p.63)。eval
都在全局作用域下执行。 这样一来,既保持了对旧网站的兼容性,也保证了一定程度的安全性。eval
时的行为直接调用和间接调用最大的区别在于他们的作用域不同:
javascript function test() { var x = 2, y = 4 console.log(eval("x + y")) // Direct call, uses local scope, result is 6 var geval = eval; console.log(eval("x + y")) // Indirect call, uses global scope, throws ReferenceError because
xis undefined }
间接调用
eval
最大的用处(可能也是唯一的实际用处)是在任意地方获取到全局对象(然而 Function('return this')() 也能做到这一点):javascript // 即使是在严格模式下也能起作用 var global = ("indirect", eval)("this");
未来,如果 Jordan Harband 的
System.global
提案能进入到标准的话,这最后一点用处也用不到了……十一、非严格模式下,赋值给未声明的变量会导致产生一个新的全局变量
Value Properties of the Global Object
平常我们使用到的 NaN,Infinity、undefined 并不是作为原始值被使用的,而是定义在全局对象上的属性名。
在 ES5 之前,这几个属性甚至可以被覆盖,直到 ES5 之后它们才被改成 non-configurable、non-writable。
然而,因为这几个属性名都不是 JavaScript 的保留字,所以可以被用来当做变量名使用。即使全局变量上的这几个属性不可被更改,我们仍可以在自己的作用域里面对这几个名字进行覆盖。
Stateful RegExps
JavaScript 中,正则对象上的函数是有状态的:
这使得这些方法难以调试,无法做到线程安全。
Brendan Eich 的说法是这些方法来自于 90 年代的 Perl 4,那时候并没有想到这么多。
未完待续...
References