跳到主要内容

原理-类型与文法

1. 类型

目前(~2021-02-26), Javascript共有八种内建类型: null, undefined, boolean, number, string, object, symbol(ES6), 以及 bigint(ES10).

大部分情况下, 基本类型直接代表了最底层的语言的实现.

所有基本类型的值都是不可改变的. 但需要注意的是, 基本类型本身和一个赋值为基本类型的变量的区别.

注意一点: 值是类型, 变量并不是, 在 js 中变量是没有类型的. 所以说 JS 是弱类型语言(变量没有类型).

1.1 类型判断: typeof

类型检测typeof可以检测除了null以外的其他类型, 一般用来检查基础类型会比较方便.

typeof 总是返回字符串:

typeof undefined === 'undefined'; // true
typeof true === 'boolean'; // true
typeof 42 === 'number'; // true
typeof '42' === 'string'; // true
typeof { life: 42 } === 'object'; // true
typeof Symbol() === 'symbol'; // true
typeof 111n === 'bigint'; // true
//除了
typeof null === 'object'; // true

那么对于null来说, 我们可以这样检测:

var a = null;

!a && typeof a === 'object'; // true

null 由于历史原因在 js 中的表现很特殊, 它是唯一一个falsy, 但是 typeof 返回 object 的基本类型.

至于为什么说typeof不适合检查复杂对象, 我们看下面这些代码:

function func1() {}
const func2 = function () {};
const func3 = new Function('name', 'console.log(name)');

const obj1 = {};
const obj2 = new Object();
const obj3 = new func1();
const obj4 = new new Function()();

console.log(
typeof Object, // "function"
typeof Function, // "function"
typeof func1, // "function"
typeof func2, // "function"
typeof func3, // "function"
typeof obj1, // "object"
typeof obj2, // "object"
typeof obj3, // "object"
typeof obj4, // "object"
);

此外, 函数在 js 中被视为对象:

function a(b, c) {
/* .. */
}
a.length; // 2
//因为你使用了两个正式命名的参数(b 和 c)声明了函数,所以“函数的长度”是 2。

Typeof 的历史

typeof null是一个从js第一版就存在的bug. 在这个版本中, 值以32位的单位存储, 包括一个小型类型标记(1-3位)和值的实际数据. 类型标记存储在单元的较低位上. 一共有5中类型:

  • 000: object, 表示这个数据是一个对象的引用
  • 1: int, 表示这个数据是一个31位的有符号整数
  • 010: double, 表示这个数据是一个双精度浮点数的引用
  • 100: string, 表示这个数据是一个字符串的引用
  • 110: boolean, 表示这个数据是一个布尔值

有两个值比较特殊:

  • undefined(JSVAL_VOID)的值是 -2^30 整形(一个超出整形范围的数)
  • null(JSVAL_NULL)是机器码空指针. 或者是: 一个对象类型标记叫上一个为零的引用.

现在我们就知道为什么type of认为null是一个对象了: 它检查了类型标记和类型标记表示的对象.

在mozilla的typeof源码中:

  1. typeof null返回object, 是因为JS内部把值的低位为0x0的标记定义为对象类型, 而null就被计算成了一个低位具有对象类型标记的值
  2. type返回function, 只要一个对象有一个不为0的call属性/方法, 或者是js_FunctionClass类型, 也就是这个对象里面有Function标记, 那么就返回function, 其他情况都返回object.

在ES6的规范解释中:

  1. 如果一个对象(Object)没有实现[[Call]]内部方法, 那么它就返回object
  2. 如果一个对象(Object)实现了[[Call]]内部方法, 那么它就返回function

[[Call]]的解释

执行于此对象关联的代码. 通过函数调用表达式调用. 内部方法的参数是一个this值和一个包含调用表达式传递给函数的参数的列表. 实现此内部方法的对象是可调用的.

简单点说, 如果一个对象支持了内部的[[Call]]方法, 那么它就可以被调用, 就变成了函数, 所以叫做函数对象.

相应的, 如果一个函数对象支持了内部的[[Construct]]方法, 那么它就可以使用new或者super来调用. 这是我们就可以把这个函数对象称为: 构造函数.

1.2 类型判断: instanceof

instanceof 本质上是查询原型链中是否存在你所要检测的类, 所以简单类型是没有原型链的, 也就直接使用 instanceof 来检查类型. instanceof 也一般用来检查复杂类型.

console.log('1' instanceof String); //false
console.log(1 instanceof Number); //false
console.log(true instanceof Boolean); //false
console.log([] instanceof Array); //true
console.log(function() {} instanceof Function); //true
console.log({} instanceof Object); //true


console.log(new Number(1) instanceof Number)//true
...

实现一个 instanceof

首先instanceof左边必须是对象, 才能找到它的原型链

其次instanceof右边必须是函数, 才会有prototype属性

然后进行迭代, 左侧对象的原型不等于右侧的prototype的时候, 沿着原型链重新赋值左边

function instance_of(L,R) {
// 验证如果为基本数据类型, 就直接返回 false
const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol']
if (baseType.includes(typeof L)){ return false }

let RP = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while(L) {
if (L === RP){
// 严格相等
return true
}
L = L.__proto__; // 没找到继续向上一层的原型链查找
}
return false
}

1.3 类型判断: constructor

constructor 也是检查类型的一种方法, 在前面的对象和原型中有简单的介绍过其原理, 所以不难明白如何使用它来检查类型:

console.log('1'.constructor === String); //true
console.log((1).constructor === Number); //true
console.log(true.constructor === Boolean); //true
console.log([].constructor === Array); //true
console.log(function() {}.constructor === Function); //true
console.log({}.constructor === Object); //true

但是要注意一点, constructor 并不是很好的检测类型方法, 因为我们有时候会有意或者无意的改变对象的 constructor指向, 导致检测结果的错误.

function Fn() {}

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor === Fn); //false
console.log(f.constructor === Array); //true

1.4 类型判断: Object.prototype.toString.call()

这是一种比较完善的解决方案了

var a = Object.prototype.toString;

console.log(a.call('aaa')); //[object String]
console.log(a.call(1)); //[object Number]
console.log(a.call(true)); //[object Boolean]
console.log(a.call(null)); //[object Null]
console.log(a.call(undefined)); //[object Undefined]
console.log(a.call([])); //[object Array]
console.log(a.call(function() {})); //[object Function]
console.log(a.call({})); //[object Object]

1.5 Array.isArray()

Array.isArray()可以用来判断对象是否为数组, 在检测array实例的时候, Array.isArray 优于 instanceof, 因为Array.isArray可以检测出iframes:

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length - 1].Array;
var arr = new xArray(1, 2, 3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr); // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

1.6 Number.isNaN()

Number.isNaN()方法确定传递的值是否为NaN和其类型是Number。它是原始的全局isNaN()的更强大的版本。

Number.isNaN(NaN);        // true
Number.isNaN(Number.NaN); // true
Number.isNaN(0 / 0) // true

// 下面这几个如果使用全局的 isNaN() 时,会返回 true。
Number.isNaN("NaN"); // false,字符串 "NaN" 不会被隐式转换成数字 NaN。
Number.isNaN(undefined); // false
Number.isNaN({}); // false
Number.isNaN("blabla"); // false

polyfill:

Number.isNaN = Number.isNaN || function(value) {
return typeof value === "number" && isNaN(value);
}

1.7 Number.isFinite()

Number.isFinite()方法用来检测传入的参数是否是一个有穷数(finite number)。

Number.isFinite(Infinity);  // false
Number.isFinite(NaN); // false
Number.isFinite(-Infinity); // false

Number.isFinite(0); // true
Number.isFinite(2e64); // true

Number.isFinite('0'); // false, 全局函数 isFinite('0') 会返回 true

polyfill:

Number.isFinite = Number.isFinite || function(value) {
return typeof value === "number" && isFinite(value);
}

2. 值

值是 JS 中最基础的部分, 但是也有一些需要注意的点, 我们分别整理一下:

2.1 Array

在 JS 中 Array 只是值的容器, 这些值可以是任何类型, 使用时有些需要注意的地方:

  1. 在 array 上使用 delete 会移除一个值槽, 但不会更新 length
var a = [];
a[0] = 1;
a[1] = '2';
a[2] = [3];

delete a[0]; //[empty,"2",[3]]
a.length; //3
  1. 不要随意的创建稀疏数组(留下或创建空的/丢失的值槽), 除非你知道自己在做什么
a[4] = '5';
a; //[empty,"2",[3],empty,"5"]
  1. 数组也是对象, 所以给数字索引或者[string]索引:
var a = [];

a[0] = 1;
a['foobar'] = 2;

a.length; // 1
a['foobar']; // 2
a.foobar; // 2

使用字符串访问的时候, 其中还可能会进行隐式类型转换, 需要特别注意:

var a = [];

a['13'] = 42;

a.length; // 14

此外, 建议不要再数组上添加属性,以免给自己或者他人挖坑.

2.2 String

注意了, JS 中的string是不可变的, string 方法上没有一个方法是可以原地修改它的内容的, 都是创建并返回一个新的 string, 这不同于 array, 许多方法是可以原地修改的.

此外, 在 js 中用位置访问字符的a[1]并不总是合法的(在老的 IE 上是不允许的, 现在可以了), 最兼容的写法是a.charAt(1).

2.3 Number

数字类型在 JS 中比较的奇妙.

  1. JS 中并不区分浮点数和整数. 所谓的整数只是一个没有小数部分的小数值, 也就是42.042是一样的.
  2. JS 的数字实现基于 IEEE754 的双精度标准(64 位 2 进制).
  3. toFixed方法
// 不合法的语法:
42.toFixed( 3 ); // SyntaxError

// 这些都是合法的:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000" 注意空格

.是个合法操作符哦

  1. 科学计数法
var onethousand = 1e3; // 代表 1 * 10^3
var onemilliononehundredthousand = 1.1e6; // 代表 1.1 * 10^6
  1. 不同进制的表示:
0xf3; // 十六进制的: 243
0xf3; // 同上

0363; // 八进制的: 243

//ES6
0o363; // 八进制的: 243
0o363; // 同上

0b11110011; // 二进制的: 243
0b11110011; // 同上
  1. 浮点数判定问题:
0.1 + 0.2 === 0.3; // false

解决方法: 使用Number.EPSILON.

Number.EPSILON被称为"机械极小值", 对于 JS 通常为:2^-52(2.220446049250313e-16)

//ES6之前部署
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2, -52);
}
//ES6!
function numbersCloseEnoughToEqual(n1, n2) {
return Math.abs(n1 - n2) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numbersCloseEnoughToEqual(a, b); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false
  1. 数字的检测方法:
  • 测试整数: Number.isInteger(..)
  • 安全整数: Number.isSafeInteger(..)
  • 检测两个值的绝对等价性(ES6):
var a = 2 / 'foo';
var b = -3 * 0;

Object.is(a, NaN); // true
Object.is(b, -0); // true

Object.is(b, 0); // false

ES6 以前的 shim:

if (!Object.is) {
Object.is = function(v1, v2) {
// 测试 `-0`
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 测试 `NaN`
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情况
return v1 === v2;
};
}
  1. BigInt(ES9)

JavaScript 中 Number.MAX_SAFE_INTEGER 表示最大安全数字,计算结果是 9007199254740991(2 ** 53 - 1)),即在这个数范围内不会出现精度丢失(小数除外)。

但是一旦超过这个范围,js 就会出现计算不准确的情况,这在大数计算的时候不得不依靠一些第三方库进行解决,因此官方提出了 BigInt 来解决此问题。

const aNumber = 111;
const aBigInt = BigInt(aNumber);
aBigInt === 111n // true
typeof aBigInt === 'bigint' // true
typeof 111 // "number"
typeof 111n // "bigint"

2.4 引用和值

在 js 中, 简单值通过值拷贝来赋予和传递: null, undefined, string, number, boolean以及symbol. 而复合值则总是通过引用传递: object,function.

3. 原生类型

原生类型:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

原生类型的每一种可以被用作一个原生类型的构造器, 但构造出来的东西并不是简单类型:

var a = new String('abc');

typeof a; // "object" ... 不是 "String"

a instanceof String; // true

Object.prototype.toString.call(a); // "[object String]"

3.1 内部[[Class]]

typeofobject的值, 一般内部有一个标签属性[[Class]]. 一般不能直接调用, 可以间接的调用Object.prototype.toString来判断类型:

Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call(/regex-literal/i); // "[object RegExp]"
Object.prototype.toString.call('abc'); // "[object String]"
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"

Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"

注意, 不存在nullundefined的原生类型构造器, 但是在内部还是可以暴露出NullUndefined的值.

3.2 封箱包装器

为了使 基本类型 访问到相对应的原生类型的定义的方法, JS 提供了封装包装器来满足这样的访问, JS 为基本类型隐含的创建了相应的原生类型对象, 因此, 我们应当总偏向于使用基本类型字面量而不是其对象形式.

开箱之前, 有一点主要注意:

var a = new Boolean(false);

a == false; //true
if (!a) {
console.log('Oops'); // 永远不会运行
}

手动封箱:

var a = 'abc';
var b = new String(a);
var c = Object(a);

typeof a; // "string"
typeof b; // "object"
typeof c; // "object"

b instanceof String; // true
c instanceof String; // true

Object.prototype.toString.call(b); // "[object String]"
Object.prototype.toString.call(c); // "[object String]"

3.3 开箱

var a = new String('abc');
var b = new Number(42);
var c = new Boolean(true);

a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true

3.4 隐式开箱

var a = new String('abc');
var b = a + ''; // `b` 拥有开箱后的基本类型值"abc"

typeof a; // "object"
typeof b; // "string"

4. 类型转换

JS 的隐式类型转换总是得到基本标量值的一种, 比如string, number或者boolean, 但是封箱不是, 它是一种显式类型转换.

4.1 JSON 字符串化

JSON.stringify(value[, replacer[, space]])

JSON.stringify可以将一个值序列化为一个 JSON 兼容的string值, 对于简单的值, JSON.stringifytoString是一样的行为.

JSON.stringify在遇到undefined, function, symbol的时候自动忽略, 如果在一个array中, 它会被替换为null, 如果在 object 属性中遇到这样的值, 这个属性会被简单地剔除掉.

  • toJSON方法会被优先调用已取得用于序列化的值. 用于返回这个object的一个 JSON 安全版本.
var o = {};

var a = {
b: 42,
c: o,
d: function() {}
};

// 在 `a` 内部制造一个循环引用
o.e = a;

// 这会因循环引用而抛出一个错误
// JSON.stringify( a );

// 自定义一个 JSON 值序列化
a.toJSON = function() {
// 序列化仅包含属性 `b`
return { b: this.b };
};

JSON.stringify(a); // "{"b":42}"

一般说,toJSON返回一个适用于字符串化的 JSON 安全的值, 由JSON.stringify处理字符串化.

此外, JSON.stringify还有第二个参数: replacer, 可以接受一个 array 或者 function, 提供一种过滤机制, 指出一个object的哪一个属性应该或者不应该被包含在序列化形式中, 来自定义个object的递归序列化行为.

如果是一个 array, 那么它的每一个元素指定了运行被包含在这个object的序列化形式中的属性名称. 如果一个属性不存在于这个列表中, 那么它就会被跳过.

如果是一个 function, 那么它会为object本身而被调用一次, 并且为这个object中的每个属性都调用一次, 每次调用传入一组 key 和 value, 跳过则返回 undefined, 否则返回被提供的 value.

var a = {
b: 42,
c: '42',
d: [1, 2, 3]
};

JSON.stringify(a, ['b', 'c']); // "{"b":42,"c":"42"}"

JSON.stringify(a, function(k, v) {
if (k !== 'c') return v;
});
// "{"b":42,"d":[1,2,3]}"

JSON.stringify的第三个可选参数接受一个正整数, 用来指示每一级缩进中应当适用多少个空格或者接受一个 string, 每一级的缩进会使用它的前十个字符:

var a = {
b: 42,
c: '42',
d: [1, 2, 3]
};

JSON.stringify(a, null, 3);
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"

JSON.stringify(a, null, '-----');
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"

4.2 Falsy

Falsy 比较少:

  • undefined
  • null
  • false
  • +0,-0,NaN
  • ""

除此之外都是 truthy.

4.3 ~

~是按位取反操作, ~x有点像-(x+1), 所以可以用在 indexOf 中来判断返回的结果.

此外, ~~可以进行小数的截断:

Math.floor(-49.6); // -50
~~-49.6; // -49

也可以使用x|0来截断一个 32 位的整数, 但是特别的注意操作符优先级问题:

~~1e20 / 10; // 166199296

1e20 | (0 / 10); // 1661992960
(1e20 | 0) / 10; // 166199296

4.4 Number 和 String 之间的隐式类型转换

number 转 string:

var a = '42';
var b = '0';

var c = 42;
var d = 0;

a + b; // "420"
c + d; // 42

var a = 42;
var b = a + '';

b; // "42"

string 转 number:

//(1)
var a = '3.14';
var b = a - 0;

b; // 3.14

//(2)
var a = [3];
var b = [1];

a - b; // 2

4.5 Boolean 的隐式转换

在下面这些语句中, 会隐式的转换为 boolean:

  1. 在一个if (..)语句中的测试表达式。
  2. 在一个for ( .. ; .. ; .. )头部的测试表达式(第二个子句)。
  3. while (..)do..while(..)循环中的测试表达式。
  4. ? :三元表达式中的测试表达式(第一个子句)。
  5. ||(“逻辑或”)&&(“逻辑与”)操作符左手边的操作数(它用作测试表达式 —— 见下面的讨论!)。

4.6 ==和===

==允许在等价性比较重进行强制转换,而===不允许强制转换

小心:

  1. NaN 永远不等于 NaN
  2. +0 等于-0

==的转换细节:

  1. 字符串和数字比较时,一般是字符串转换为数字
  2. 任何东西和 boolean 进行比较,boolean 都会被转换为数字,然后进行比较(true=>1,false=>0)
  3. null 和 undefined 是等价的
  4. 一个对象和另一个非对象进行比较会对对象进行ToPrimitivie(toString/valueOf),以及拆箱

4.7 边界情况

  1. 一个拥有其他值的数字:
Number.prototype.valueOf = function() {
return 3;
};

new Number(2) == 3; // true
  1. 进一步修改 valueOf 来达到一种邪恶的效果:
var i = 2;

Number.prototype.valueOf = function() {
return i++;
};

var a = new Number(42);

if (a == 2 && a == 3) {
console.log('Yep, this happened.');
}
  1. 双等号的奇怪判断(注意那些噢出来的):
'0' == null; // false
'0' == undefined; // false
'0' == false; // true -- 噢!
'0' == NaN; // false
'0' == 0; // true
'0' == ''; // false

false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 噢!
false == ''; // true -- 噢!
false == []; // true -- 噢!
false == {}; // false

'' == null; // false
'' == undefined; // false
'' == NaN; // false
'' == 0; // true -- 噢!
'' == []; // true -- 噢!
'' == {}; // false

0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 噢!
0 == {}; // false

[] == ![]; // true
0 == '\n'; // true

在使用==的时候, 注意两点可以避免大部分情况:

  1. 不要和false/true直接进行比较
  2. 不要和[]/""/0直接进行比较

4.8 不等号比较

不等号在进行比较的时候也发生了隐式转换:

//两边转换为了字符串,进行字典顺序的比较

var a = ['42'];
var b = ['043'];

a < b; // false

var a = [4, 2];
var b = [0, 4, 3];

a < b; // false

//第二种情况
var a = { b: 42 };
var b = { b: 43 };

a < b; // false
a == b; // false
a > b; // false

a <= b; // true
a >= b; // true

在 JS 的语言中, 对于 a<=b, 它实际上首先对 b<a 求值, 然后反转那个结果, 因为b<a也是 false, 所以 a<=b 的结果是 true.

因为没有严格的关系型比较, 所以我们不能防止这样的比较发生隐含的强制转换.

下面是一道例题:

var a = {
i: 1,
toString() {
return a.i++;
}
};

if (a == 1 && a == 2 && a == 3) {
console.log(1);
}

5. 文法

js 的文法以一种结构化的方式来描述语法如何组合在一起形成结构良好,合法的程序, 离开文法, 语法就很难完整的表述好.

5.1 语句完成值

我们在使用 Chrome 的时候会发现, 每个语句都有一个完成值, 即使返回 undefined.

但是我们没有任何简单的语法/文法来捕获一个语句的完成值来赋值给另一个变量.

//这种代码是不工作的
var a, b;
a = if (true) {
b = 4 + 38;
};

eval或许可以, 但不推荐, 在 ES7 的提案中有一个do试图实现这种表达:

var a, b;

a = do {
if (true) {
b = 4 + 38;
}
};

a; // 42

5.2 表达式副作用

大多数表达式是没有副作用的, 少部分的会有:

var a = 42;

a++; // 42
a; // 43

++a; // 44
a; // 44

//注意执行的顺序
var a = 42;
var b = a++;

a; // 43
b; // 42

使用,()可以将多个语句连成一个单独的语句:

var a = 42,
b;
b = (a++, a);

a; // 43
b; // 43

表达式(a++, a)意味着第二个a语句表达式会在第一个a++语句表达式的后副作用之后进行求值, 为 b 的赋值返回 43.

另一个有副作用的操作符:delete

var obj = {
a: 42
};

obj.a; // 42
delete obj.a; // true
obj.a; // undefined

最后一个存在副作用的例子是=, 虽然看起来不像, 在下面这种情况中确实会影响对代码的理解:

var a;

a = 42; // 42
a; // 42

var a, b, c;

a = b = c = 42;

5.3 上下文规则

5.3.1 {}大括号

{}主要出现在字面量对象, 函数, 逻辑关键字, 解构赋值中. ES6 以前{}不存在块作用域, 但是配合 ES6 的let关键字, 我们可以创建出块作用域.

{}单独存在的时候(没有进行赋值), 它并不是一个空的字面量对象, 而是普通的代码块.

[]+{};//"[object Object]"
// => ""+Object=>"Object"

{}+[];//0
// => +[] =>0

5.3.2 带标签的 for 循环

// 用`foo`标记的循环
foo: for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
// 每当循环相遇,就继续外层循环
if (j == i) {
// 跳到被`foo`标记的循环的下一次迭代
continue foo;
}

// 跳过奇数的乘积
if ((j * i) % 2 == 1) {
// 内层循环的普通(没有被标记的) `continue`
continue;
}

console.log(i, j);
}
}
// 1 0
// 2 0
// 2 1
// 3 0
// 3 2

5.4 操作符优先级

JS 语言规范没有将它的操作符优先级罗列在一个方便, 单独的位置. MDN 整理了一份

一般来说:

  • &&比||的优先级高
  • ||优先级比?:高
  • 一般来说,操作符操作顺序看 分组从左边发生还是右边发生
  • 一般来说, &&和||是左结合的
  • ?:是右结合的
true ? false : true ? true : false; // false

true ? false : true ? true : false; // false
(true ? false : true) ? true : false; // false

5.5 自动分号

ASI(自动分号插入): ASI 允许 JS 容忍那些通常被认为是不需要;的特定地方省略;. 并且仅在换行存在时起作用. 分号不会被插入一行中间

推荐在需要使用分号的地方使用分号,把对 ASI 的臆测限制到最小.

参考链接