类型和语法


1.类型

  1. JavaScript 有 七 种 内 置 类 型:null、undefined、boolean、number、string、object 和 symbol,可以使用 typeof 运算符来查看。
  2. 变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
  3. 很多开发人员将undefined 和 undeclared 混为一谈,但在JavaScript 中它们是两码事。 undefined 是值的一种。undeclared 则表示变量还没有被声明过。
  4. 遗憾的是,JavaScript 却将它们混为一谈,在我们试图访问”undeclared” 变量时这样报 错:ReferenceError: a is not defined,并且typeof 对 undefined 和 undeclared 变量都返回 “undefined”。
  5. 然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的 办法。

内置类型

JavaScript七种内置类型

  • 空值(null)
  • 未定义(undefined)
  • 布尔值( boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol,ES6 中新增)

除对象之外,其他统称为“基本类型”。

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 null          === "object";    // true
//null 是基本类型中唯一的一个“假值,正确的返回结果应该是 "null",但这个bug 由来已久

typeof function a(){ /* .. */ } === "function"; // true
//函数是 object 的一个“子类型”。具体来说,函数是“可调用对象”,它有一个内部属 性 [[Call]],该属性使其可以被调用。

typeof [1,2,3] === "object"; // true
//数组也是对象。确切地说,它也是 object 的一个“子类型”,数组的 元素按数字顺序来进行索

值和类型

  • JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
  • 也就是说JavaScript 不做“类型强制”;也就是说,语言引擎不要求变量总是 持有与其初始值同类型的值。一个变量可以现在被赋值为字符串类型值,随后又被赋值为 数字类型值。
  • 所以在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类 型,因为 JavaScript 中的变量没有类型

undefined 和 undeclared

  1. 已在作用域中声明但还没有赋值的变量,是 undefined 的。
  2. 相反,还没有在作用域中声明 过的变量,是 undeclared 的。
var a; 
 typeof a; // "undefined" 
 typeof b; // "undefined"

 //对于 undeclared(或者 not defined)变量,typeof 照样返回 "undefined"。请注意虽然 b 是 一个 undeclared 变量,但 typeof b 并没有报错。这是因为 typeof 有一个特殊的安全防范 机制。

2.值

  1. JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的 行为特征不同,在将字符作为数组来处理时需要特别小心。JavaScript 中的数字包括“整 数”和“浮点型”。
  2. 基本类型中定义了几个特殊的值。
    null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之 前默认值都是 undefined。void 运算符返回 undefined。
  3. 数 字 类 型 有 几 个 特 殊 值, 包 括NaN( 意 指“not a number”, 更 确 切 地 说 是“invalid number”)、 +Infinity、-Infinity 和 -0。
  4. 简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等) 通过引用复制来赋值 / 传递。JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不 能指向别的变量 / 引用,只能指向值。

数组

  1. 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串、 数字、对象(object),甚至是其他数组(多维数组)
  2. 对数组声明后即可向其中加入值,不需要预先设定大小

类数组

一些 DOM 查询操作会返回 DOM 元素列表,它们并非真正意义上的数组,但十分 类似。另一个例子是通过 arguments 对象(类数组)将函数的参数当作列表来访问(从 ES6 开始已废止)

function foo() {    
    var arr = Array.prototype.slice.call( arguments );    
    arr.push( "bam" );  
    console.log( arr ); 
} 
foo( "bar", "baz" ); // ["bar","baz","bam"]
//工具函数 slice(..) 经常被用于这类转换,slice() 返回参数列表(上例中是一个类数组)的一个数组复本。

//用 ES6 中的内置工具函数 Array.from(..) 也能实现同样的功能:
var arr = Array.from( arguments ); 

字符串

  1. 字符串和数组很相似,它是类数组,都有 length 属性以及 indexOf(..)(从 ES5 开始数组支持此方法)和 concat(..) 方法
  2. JavaScript 中字符串是不可变的,而数组是可变的。字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符 串。而数组的成员函数都是在其原始值上进行操作。
  3. 许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”数 组的非变更方法来处理字符串
  4. 另一个不同点在于字符串反转(JavaScript 面试常见问题)。数组有一个字符串没有的可变
    更成员函数 reverse()。我们无法“借用”数组的可变更成员函数,因为字符串是不可变的
//字符串反转一个变通(破解)的办法是先将字符串转换为数组,待处理完后再将结果转换回字符串
var c = a      
    .split( "" )      // 将a的值转换为字符数组    
    .reverse()       // 将数组中的字符进行倒转   
    .join( "" );    // 将数组中的字符拼接回字符串   

c; // "oof"
  1. 如果需要经常以字符数组的方式来处理字符串的话,倒不如直接使用数组。这样就不用在 字符串和数组之间来回折腾。可以在需要时使用 join(“”) 将字符数组转换为字符串

数字

  1. JavaScript 只有一种数值类型:number(数字),包括“整数”和带小数的十进制数。此处 “整数”之所以加引号是因为和其他语言不同,JavaScript 没有真正意义上的整数
  2. JavaScript 中的“整数”就是没有小数的十进制数。所以 42.0 即等同于“整数”42。

数字语法

特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。

a;                  // 50000000000 
a.toExponential();  // "5e+10" 

tofixed(..) 方法可指定小数部分的显示位数

var a = 42.59; 
a.toFixed( 0 ); // "43" 
a.toFixed( 4 ); // "42.5900"
42.toFixed( 3 );    // SyntaxError 无效语法
42 .toFixed(3); // "42.000" 空格有效
42..toFixed( 3 );   // "42.000"  
//第一个 . 被视为 number 的一部分,第二个 . 是属性访问 运算符

toPrecision(..) 方法用来指定有效数位的显示位数

var a = 42.59; 
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"

较小数值

0.1 + 0.2 === 0.3; // false
//二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们相加的结果并非刚好等于 0.3,而是一个比较接近的数字 0.30000000000000004,所以条件判断结果为 false。

如何做到完全精确

也就是怎样来判断 0.1 + 0.2 和 0.3 是否相等?

最常见的方法是设置一个误差范围值,通常称为“机器精度”(machine epsilon), 对 JavaScript 的数字来说,这个值通常是 2^-52

  1. 从 ES6 开始,该值定义在 Number.EPSILON 中,我们可以直接拿来用
  2. 为 ES6 之前 的版本写 polyfill:
if (!Number.EPSILON) {  
    Number.EPSILON = Math.pow(2,-52);    
}
可以使用 Number.EPSILON 来比较两个数字是否相等(在指定的误差范围内):
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 

整数检测

要检测一个值是否是整数,可以使用 ES6 中的 Number.isInteger(..) 方法

Number.isInteger( 42 );     // true
Number.isInteger( 42.000 ); // true 
Number.isInteger( 42.3 );   // false

要检测一个值是否是安全的整数,可以使用 ES6 中的 Number.isSafeInteger(..) 方法

Number.isSafeInteger( Number.MAX_SAFE_INTEGER );    // true
Number.isSafeInteger( Math.pow( 2, 53 ) );          // false 
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 );      // true

特殊数值

不是值的值

  1. undefined 类型只有一个值,即 undefined。undefined 指没有值
  2. null 类型也只有一个值,即 null。它们的名 称既是类型也是值。null 指空值
  3. undefined 和 null 常被用来表示“空的”值或“不是值”的值
  4. null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。但是永远不要重新定义 undefined。

void 运算符

undefined 是一个内置标识符(除非被重新定义),它的值为 undefined, 通过 void 运算符即可得到该值

表达式 void ___ 没有返回值,因此返回结果是 undefined。按惯例我们用 void 0 来获得 undefined,void 并不改变表达式的结果, 只是让表达式不返回值

var a = 42; 
console.log( void a, a ); // undefined 42

特殊的数字

  1. 不是数字的数字

如果数学运算的操作数不是数字类型(或者无法解析为常规的十进制或十六进制数字), 就无法返回一个有效的数字,这种情况下返回值为 NaN。

NaN 意指“不是一个数字”(not a number),这个名字容易引起误会。NaN是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误 情况,即“执行数学运算没有成功,这是失败后返回的结果”。

var a = 2 / "foo";      // NaN 
typeof a === "number";  // true   NaN仍然是数字类型

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不 成立)的值。而 NaN != NaN 为 true。

var a = 2 / "foo"; 
a == NaN;   // false
a === NaN;  // false

可以使用内建的全局工具函数 isNaN(..) 来判断一个值是否是 NaN

var a = 2 / "foo"; 
isNaN( a ); // true

isNaN(..) 有一个严重的缺陷,它的检查方式过于死板,就 是“检查参数是否不是 NaN,也不是数字”。但是这样做的结果并不太准确

var b = "foo"; 
window.isNaN( b ); // true

解决这个问题可以使用ES6工具函数 Number.isNaN(..)

  // Number.isNaN(..)实现原理
  return (typeof n === "number" &&  window.isNaN( n ));   

还有一个方法是利用 NaN 不等于自身这个特点。NaN 是 JavaScript 中唯 一一个不等于自身的值

if (!Number.isNaN) {    
    Number.isNaN = function(n) {     
        return n !== n;   
        };
    }
  1. 无穷数

JavaScript 使用有限数字表示法,所以和纯粹的数学运算不同,JavaScript 的运算结果有可能溢出,此时结果为 Infinity 或者 -Infinity。

var a = 1 / 0;  // Infinity
var b = -1 / 0; // -Infinity

计算结果一旦溢出为无穷数(infinity)就无法再得到有穷数

Infinity/ Infinity 是一个未定义操作,结果为 NaN。有穷正数除以 Infinity,结果是 0。

  1. 零值

JavaScript 有一个常规的 0(也叫作 +0)和一个 -0。

var a = 0 / -3; // -0 
var b = 0 * -3; // -0   加法和减法运算不会得到负

对负零进行字符串化会返回 “0”,如果反过来将其从字符串转换为数字,得到的结果是准确的

var a = 0 / -3; 
a + "";          // "0" 
+"-0";           // -0 
//JSON.stringify(-0) 返回 "0",而 JSON.parse("-0") 返回 -0
-0 == 0;    // true
//区分-0和0的原理
 n = Number( n );     
 return (n === 0) && (1 / n === -Infinity); 

为什么要-0

有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位 (sign)用来代表其他信息(比如移动的方向)。此时如果一个值为 0 的变量失去了它的符号位,它的方向信息就会丢失。所以保留 0 值的符号位可以防止这类情况发生。

值和引用

  1. 简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。

    var a = 2; 
    var b = a; // b是a的值的一个副本
    b++;
    a; // 2
    b; // 3 
  2. 复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则总是通过引用复制的方式来赋值 / 传递。

    var c = [1,2,3];
    var d = c; // d是[1,2,3]的一个引用 d.push( 4 );
    c; // [1,2,3,4]
    d; // [1,2,3,4]

    由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向

    var a = [1,2,3]; 
    var b = a; 
    b = [4,5,6];
    a; // [1,2,3]
    b; // [4,5,6]
    //函数传参时不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x 共同指向的值。
    function foo(x) { 
        x.push( 4 );   
        x; // [1,2,3,4] 
    
        // 然后   
        x = [4,5,6];  
        x.push( 7 );  
        x; // [4,5,6,7]
        } 
    var a = [1,2,3]; 
    foo( a ); 
    
    a; // 是[1,2,3,4],不是 [4,5,6,7]
    //传递的是指向数字对象的引用复本,但我们并不能通过它来更改其 中的基本类型值,原因是标量基本类型值是不可更改的,除非创建一个包含新值的数字对象。
    function foo(x) {  
        x = x + 1;     
        x; // 3  
        } 
     var a = 2; 
     var b = new Number( a ); // Object(a)也一样 
     foo( b ); console.log( b ); // 是2,不是 3

我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。如果想要改变传递的类型,有以下两种方法

  1. 如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本可利用slice()返回一个浅副本,这样传递的 就不再是原始值。
  2. 相反,如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递。

3.原始函数

  1. JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim() 和 Array#concat(..))。
  2. 对于简单标量基本类型值,比如 “abc”,如果要访问它的 length 属性或 String.prototype 方法,JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实 现对这些属性和方法的访问。

常用的原生函数有:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()——ES6 中新加入的!
    实际上,它们就是内建函数

内部属性 [[Class]]

所有 typeof 返回值为 “object” 的对象(如数组)都包含一个内部属性 [[Class]](我们可 以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问, 一般通过 Object.prototype.toString(..) 来查看。

Object.prototype.toString.call( [1,2,3] ); // "[object Array]" 

Object.prototype.toString.call( /regex-literal/i ); // "[object RegExp]

封装对象包装

封装对象(object wrapper)扮演着十分重要的角色。由于基本类型值没有.length 和 .toString() 这样的属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为基本类型值包装一个封装对象

一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript 引擎自己决定什 么时候应该使用封装对象

拆封

  1. 如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数
var a = new String( "abc" ); 
a.valueOf(); // "abc" 
  1. 在需要用到封装对象中的基本类型值的地方会发生隐式拆封。(即强制类型转换)
var a = new String( "abc" );
var b = a + ""; // b的值为"abc" 

4.强制类型转换

  1. JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。
  2. 强制类型转换常常为人诟病,但实际上很多时候它们是非常有用的。作为有使命感的 JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。
  3. 显式强制类型转换明确告诉我们哪里发生了类型转换,有助于提高代码可读性和可维 护性。
  4. 隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转 换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。
  5. 在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知 其然,还要知其所以然,并努力让代码清晰易读。

值类型转换

将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐 式的情况称为强制类型转换(coercion)。JavaScript 中的强制类型转换总是返回标量基本类型值

抽象值操作

ToString

抽象操作 ToString,它负责处理非字符串到字符串的强制类型转换。

基本类型值的字符串化规则为:null 转换为 “null”,undefined 转换为 “undefined”,true 转换为 “true”。数字的字符串化则遵循通用规则,不过极小和极大的数字使用指数形式

对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回 内部属性 [[Class]] 的值,如 “[object Object]”。

数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 “,” 连接起 来

var a = [1,2,3]; 
a.toString(); // "1,2,3"

工具函数 JSON.stringify(..)

(1) 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。

(2) 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符 串化前调用,以便将对象转换为安全的 JSON 值。

ToNumber

将非数字值当作数字来使用,其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。

Number( "" );               // 0 
Number( [] );               // 0 
Number( [ "abc" ] );        // NaN

ToBoolean

  1. 假值

  2. undefined

  3. null

  4. false

  5. +0、-0

  6. NaN

  7. “”

假值列表以 外的值都是真值

  1. 假值对象

document.all

  1. 真值

真值就是假值列表之外的值。

var a = "false"; 
var b = "0"; 
var c = "''"; 
var d = Boolean( a && b && c ); 
d;//true
var a = [];             // 空数组——是真值还是假值? 
var b = {};             // 空对象——是真值还是假值?
var c = function(){};   // 空函数——是真值还是假值? 
var d = Boolean( a && b && c ); 
d;//true

真值列表可以无限长,无法一一列举,所以我们只能用假值列表作为参考

显式强制类型转换

字符串和数字之间的显式转换

  1. String(..) 遵循前面讲过的 ToString 规则,将值转换为字符串基本类型。
  2. Number(..) 遵循 前面讲过的 ToNumber 规则,将值转换为数字基本类型。
  3. 一元运算符 - 和 + 会将操作 数显式强制类型转换为数字

日期显式转换为数字

一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为 Unix 时间戳,以微秒为单位

var timestamp = +new Date();
var timestamp = new Date().getTime(); 
var timestamp = Date.now();

~ 运算符

~ 运算符(即字位操作“非”)

~ 返回 2 的补码,所以~x 大致等同于 -(x+1)

~42;    // -(42+1) ==> -43

~ 和 indexOf() 一起可以将结果强制类型转换(实际 上仅仅是转换)为真 / 假值

var a = "Hello World"; 
if (a.indexOf( "lo" ) >= 0) {   // true     // 找到匹配! } 
if (a.indexOf( "ol" ) == -1) {  // true     // 没有找到匹配! }

>= 0 和 == -1 这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实 现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。
var a = "Hello World"; 
~a.indexOf( "lo" );         // -4   <-- 真值! 
if (~a.indexOf( "lo" )) {   // true     // 找到匹配! }

~a.indexOf( "ol" );         // 0    <-- 假值! 
!~a.indexOf( "ol" );        // true 
if (!~a.indexOf( "ol" )) {  // true     // 没有找到匹配! }

如果 indexOf(..) 返回 -1,~ 将其转换为假值 0,其他情况一律转换为真值。

显式解析数字字符串

解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。

var b = "42px"; 
parseInt( b );  // 42

parseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是 没有用的,比如 true、function(){...} 和 [1,2,3]。

显式转换为布尔值

  1. Boolean(..) 是显式的,但并不常用
  2. 显式强制类型转换为布尔值最常用的方法是 !!,因为第二个 ! 会将结果反转回原值
  3. 一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将 真值反转为假值(或者将假值反转为真值)

隐式强制类型转换

字符串和数字之间的隐式强制类型转换

  1. 数字强制类型转换为字符串

如果 + 的其中一个操作数是字符串, 则执行字符串拼接;否则执行数字加法。

var a = 42; 
var b = a + ""; 

b; // "42"

a + “”(隐式)和前面的String(a)(显式)之间有一个细微的差别需要注意。根据 ToPrimitive 抽象操作规则,a + “” 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象 操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。

var a = {   
    valueOf: function() { return 42; },  
    toString: function() { return 4; } }; 
 a + "";         // "42" 
String( a );    // "4"
  1. 字符串强制类型转换为数字的情况
var a = "3.14"; 
var b = a - 0; 
b; // 3.14

- 是数字减法运算符,因此 a - 0 会将 a 强制类型转换为数字。也可以使用 a * 1 和 a / 1,因为这两个运算符也只适用于数字,只不过这样的用法不太常见。

对象的 - 操作与 + 类似

var a = [3];
var b = [1]; 
a - b; // 2

为了执行减法运算,a 和 b 都需要被转换为数字,它们首先被转换为字符串(通过强制类型转换toString()),然后再转换为数字。

隐式强制类型转换为布尔值

  • if (..) 语句中的条件判断表达式。
  • for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
  • while (..) 和 do..while(..) 循环中的条件判断表达式。
  • ? : 中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。

 || 和 &&

在JavaScript 中的表现也和在其他语言中不太一样,“逻辑运算符”,因为这不太准确。称它们为“选择器运算符”更恰当。它们的返回值是两个操作数中的一个(且仅一个)。即选择两个操作数中的一个,然后返回它的值

var a = 42; 
var b = "abc";
var c = null; 
a || b;     // 42  
a && b;     // "abc" 

c || b;     // "abc"  
c && b;     // null
  1. 对于|| 来说,如果条件判断结果为true 就返回第一个操作数(a 和 c)的值,如果为 false 就返回第二个操作数(b)的值。应用赋予默认值
  2. && 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返 回第一个操作数(a 和 c)的值。应用守护运算符

符号的强制类型转换

ES6 允许 从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误

var s1 = Symbol( "cool" ); 
String( s1 );     // "Symbol(cool)" 

var s2 = Symbol( "not cool" );
s2 + "";      // TypeError

宽松相等和严格相等

  • “== 检查值是否相等,=== 检查值和类型是否相等” 说法不准确
  • 正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”
  • 如果两个值的类型不同,我们就需要考虑有没有强制类型转换的必要,有就用 ==,没有就
    用 ===,不用在乎性能。
  • == 和 === 都会检查操作数的类型。区别在于操作数类型不同时它们的处理方式不同。

抽象相等

  1. 如果两个值的类型相同,就仅比较它们是否相等。例如,42 等于 42,”abc” 等于 “abc”。
  2. 两个对象指向同一个值时 即视为相等,不发生强制类型转换。
  3. == 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之 一或两者都转换为相同的类型后再进行比较。

字符串和数字之间的相等比较

var a = 42;
var b = "42"; 
a === b;    // false 
a == b;     // true

因为没有强制类型转换,所以 a === b 为 false,42 和 "42" 不相等。
而 a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型 转换。

(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。

(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

其他类型和布尔类型之间的相等比较

== 最容易出错的一个地方是 true 和 false 与其他类型之间的相等比较。

var a = "42";
var b = true; 
a == b; // false

将 true 强制类型转换为 1,变成 1 == "42",二者的 类型仍然不同,"42" 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false。反过来也一样

(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;

(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

建议:无论什么情况下都不要使用 == true 和 == false。请注意,这里说的只是 ==,=== true 和 === false 不允许强制类型转换可以使用

null 和 undefined 之间的相等比较

(1) 如果 x 为 null,y 为 undefined,则结果为 true。

(2) 如果 x 为 undefined,y 为 null,则结果为 true。

在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种 情况。

var a = null; 
var b;
a == b;     // true
a == false; // false
b == false; // false

null 和 undefined 之间的强制类型转换是安全可靠的,通过这种方式将 null 和 undefined 作为等价值来处理比较好。

var a = doSomething(); 
if (a == null) {     // .. }
var a = doSomething(); 
if (a === undefined || a === null) {     // .. }

对象和非对象之间的相等比较

(1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;

(2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。

var a = "abc"; 
var b = Object( a );    // 和new String( a )一样 

a === b;                // false
a == b;                 // true
因为 b 通过 ToPromitive 进行强制类型转换(也称为“拆封”),并返回标量基本类型值 "abc",与 a 相等。
var a = null; 
var b = Object( a );    // 和Object()一样
a == b;                 // false 
var c = undefined;      
var d = Object( c );    // 和Object()一样 
c == d;                // false 
var e = NaN;            
var f = Object( e );    // 和new Number( e )一样 
e == f;                 // false

因为没有对应的封装对象,所以 null 和 undefined 不能够被封装(boxed), Object(null) 和 Object() 均返回一个常规对象。
NaN 能够被封装为数字封装对象,但拆封之后 NaN == NaN 返回 false,因为 NaN 不等于 NaN 

比较少见的情况

返回其他数字

Number.prototype.valueOf = function() {     return 3; }; 
new Number( 2 ) == 3;   // true

Number(2) 涉及 ToPrimitive 强制类型 转换,因此会调用 valueOf()。
if (a == 2 && a == 3) {     // .. }

假值的相等比较

"0" == false;          // true
false == 0;            // true 
false == "";           // true 
false == [];           // true
"" == 0;               // true 
"" == [];              // true 
0 == [];               // true 

极端情况

[] == ![]   // true
2 == [2];       // true
"" == [null];   // true

第一行中的 [2] 会转换为 "2",然后通过 ToNumber 转换为 2。第二行中的 [null] 会直接转 换为 ""。
0 == "\n";  // true
42 == "43";                        // false 
"foo" == 42;                       // false 
"true" == true;                    // false 

42 == "42";                        // true 
"foo" == [ "foo" ];                // true

完整性检查

"" == 0;               // true 
"" == [];              // true 
0 == [];               // true 

安全运用隐式强制类型转换

我们要对 == 两边的值认真推敲,以下两个原则可以让我们有效地避免出错。这时最好用 === 来避免不经意的强制类型转换。

  1. 如果两边的值中有 true 或者 false,千万不要使用 ==。
  2. 如果两边的值中有 []、”” 或者 0,尽量不要使用 ==。

抽象关系比较

  1. 比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强 制类型转换为数字来进行比较。

    var a = [ 42 ]; 
    var b = [ "43" ]; 
    a < b;  // true
    b < a;  // false

如果比较双方都是字符串,则按字母顺序来进行比较

var a = [ "42" ]; 
var b = [ "043" ];  
a < b;  // false
var a = { b: 42 }; 
var b = { b: 43 }; 
a < b;  // false

因为 a 是 [object Object],b 也是 [object Object],所以按照字母顺序 a < b 并不成立。
var a = { b: 42 };
var b = { b: 43 }; 
a < b;  // false
a == b; // false 
a > b;  // false 

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

因为根据规范 a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所 以 a <= b 的结果是 true。
实际上 JavaScript 中 <= 是 “不大于”的意思(即 !(a > b),处理为 !(b < a))。同理 a >= b 处理为 b <= a。

相等比较有严格相等,关系比较却没有“严格关系比较”。 也就是说如果要避免 a < b 中发生隐式强制类型转换,我们只能确保 a 和 b 为相同的类型, 除此之外别无他法

5.语法

  1. JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。从长远来看,这有 助于更深入地掌握这门语言。
  2. 语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表 达式可以是简单独立的,否则可能会产生副作用。
  3. JavaScript 语法规则之上是语义规则(也称作上下文)。例如,{ } 在不同情况下的意思不 尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。
  4. JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的 组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。
  5. ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制,它会在需要的地方自动插 入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略), 或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。
  6. JavaScript 中有很多错误类型,分为两大类:早期错误(编译时错误,无法被捕获)和运 行时错误(可以通过 try..catch 来捕获)。所有语法错误都是早期错误,程序有语法错误 则无法运行。
  7. 函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们 挖了不少坑。因此,尽量不要使用 arguments,如果非用不可,也切勿同时使用 arguments 和其对应的命名参数。
  8. finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑, 特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌 弄巧成拙。
  9. switch 相对于 if..else if.. 来说更为简洁。需要注意的一点是,如果对其理解得不够透 彻,稍不注意就很容易出错。

文章作者: XiaoQi
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XiaoQi !
 上一篇
Promise Promise
JavaScript 执行机制 javascript是一门单线程语言不能同时处理多个任务,把任务分成了同步和异步。 事件循环Event Loop事件循环是js实现异步的一种方法,也是js的执行机制。 同步和异步任务分别进入不同的执行”场所
2020-08-04
下一篇 
对象和类 对象和类
对象 JavaScript 中的对象有字面形式(比如 var a = { .. })和构造形式(比如 var a = new Array(..))。字面形式更常用,不过有时候构造形式可以提供更多选项。 许多人都以为“JavaScript
2020-08-01
  目录