本篇文章是根据阅读《你不知道的JavaScript 中卷》个人总结所得,如若文中的部分观点有不恰当中出还请在评论区中指出😃
类型
JavaScript中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
在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
// ES6中新加入的类型
typeof Symbol() === "symbol"; // true
除对象外,其它统称基本类型。
在基本类型中除了null
以外,都可以利用typeof
运算符进行值的类型查看
(typeof的返回值是字符串),
而对象(object
)有些许特殊,只要是对象,利用typeof
进行值的类型判断时都会返回"object"
(function除外),在JavaScript中对象有很多(Atrray,Function,Date,Error等),无法对其进行准确的判断,此时可以使用Object原型对象上的toString方法
去进行类型的准确判断(基本数据类型也可使用次方法判断出数据类型)。
let val = function name(params) { };
// 堪称万能方法!
// 这个方法也可以将null类型准确的判断出来
let str = Object.prototype.toString.call(val); // "[object Function]"
let typeString = str.substring(8, str.length - 1); // "Function"
特殊的null
JavaScript中typeof null
返回"object"
的行为是一个历史遗留问题,它存在于JavaScript的早期版本中,并且由于兼容性原因一直保留至今,在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。尽管这看起来不符合直觉,但这个行为已经被定义为标准,因此在大多数现代JavaScript引擎中仍然如此。
let val = null;
// 这个方法也可以将null类型准确的判断出来
let str = Object.prototype.toString.call(val); // "[object Null]"
let typeString = str.substring(8, str.length - 1); // "Null"
值
数组
和其他强类型语言不同,在JavaScript中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的)。
let a = [1, "2", [3]];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
稀疏数组
含有空白或空缺单元的数组称之为稀疏数组。
let a = []; // 稀疏数组
a[0] = 1;
console.log(a); // [1]
console.log(a[2]); // undefined
a[1]
的值为undefined
,但这与将其显式赋值为undefined
(a[1] = undefined)还是有所区别
。
数组通过数字进行索引,但它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内
)
let a = [];
a[0] = 1;
a["foobar"] = 2;
// 除了数字索引外,不更改数组中length属性的属性值。
console.log(a.length); // 1
console.log(a["foobar"]); // 2
console.log(a.foobar); // 2
如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。
let a = [];
a["13"] = 42; // 不推荐
console.log(a.length); // 14
字符串
字符串
和数组的确很相似,它们都是类数组
,都有length属性
以及indexOf(..)
(从ES5开始数组支持此方法)和concat(..)
方法(数组合并),JavaScript中字符串
是不可变
的,而数组
是可变
的。
let str= "syh"
// 可以通过下标的方式访问字符
console.log(str[0]); // "s"
// JavaScript中字符串是不可变的
str[1] = "M"; // 无法修改!!
console.log(str); // "syh"
访问字符串中字符的正确姿势😉。
let str= "syh"
console.log(str.charAt(0)); // “s”
数字
四舍五入?
tofixed(..)方法
可指定小数部分的显示位数,输出结果是给定数字的字符串形式
,如果指定的小数部分的显示位数多于实际位数
就用0补齐
。
let a = 42.59;
a.toFixed(0); // "43"
a.toFixed(1); // "42.6"
a.toFixed(2); // "42.59"
a.toFixed(3); // "42.590"
a.toFixed(4); // "42.5900"
😱误区:
😱
a.toFixed(1); // "42.6"
看似进行了四舍五入,实则并非如此!!toFixed() 方法遵循四舍六入五取偶
的规则。 规则是:
-
当
舍去位
的数值< 5时
,直接舍去
-
当舍去位的数值
>= 6时
,在舍去的同时向前进一位
-
当舍去位的数值
= 5时:
5后不为空且不全为0
,在舍去的同时向前进一位
5后为空或全为0:
5前数值为奇数
,则在舍去的同时向前进一位
5前数值为偶数
,则直接舍去
let num1 = 0.865;
// 舍去位为 5
console.log(num1.toFixed(2)); // "0.86"
let num2 = 0.8675;
// 舍去位为 7
console.log(num1.toFixed(2)); // "0.87"
如若有
四舍五入
的需求扔需使用 Math.round()
这个专业的方法,这个方法返回
的值的类型是number类型
,而非toFixed方法返回的string类型。
let num1 = 0.865;
console.log(Math.round(num1 * 100) / 100); // 0.87 ==> number 类型
console.log(typeof (Math.round(num1 * 100) / 100)); // "number"
数字字面量与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"
二进制、八进制、十六进制
0o363; // 243的八进制
0O363; // 同上
0b11110011; // 243的二进制
0B11110011; // 同上
0xf3; // 243的十六进制
0Xf3; // 同上
考虑到代码的易读性,不推荐使用0O363格式,因为0和大写字母O在一起容易混淆。建议尽量使用小写的0x、0b和0o
。
undefined与null
undefined
类型只有一个值,即undefined。null
类型也只有一个值,即null。它们的名称既是类型也是值。
- null指空值(empty value)
- undefined指没有值(missing value)
或者:
undefined指从未赋值
null指曾赋过值,但是目前没有值
null是
一个特殊关键字
,不是标识符
,我们不能
将其当作变量来使用和赋值
。然而undefined
却是
一个标识符
,可以被当作变量来使用和赋值
(及其不建议,因为代码会变得非常糟糕!)。
function foo() {
// 为全局作用域下的 undefined 赋值
undefined = 2; // 非常糟糕的做法!
}
foo();
// 严格模式下 全局作用域下的 undefined 不允许被修改,它是只读属性
function foo() {
"use strict";
undefined = 2; // TypeError! Cannot assign to read only property 'undefined' of object
}
foo();
在非严格和严格两种模式下
,我们可以声明一个名为undefined的局部变量
。再次强调最好不要这样做!
function foo() {
"use strict";
var undefined = 2;
console.log(undefined); // 2
}
foo();
void运算符
通过void运算符即可得到undefined。
按惯例我们用void 0来获得undefined
(这主要源自C语言,当然使用void true或其他void表达式也是可以的
)。void 0、void 1和undefined之间并没有实质上的区别
。
NaN
NaN
意指“不是一个数字”(not a number),当无法返回一个有效的数字
时,这种情况下返回值为NaN
,代表着执行数学运算没有成功
。可以将它理解为“无效数值”“失败数值”或者“坏数值”。
let a = 2 / "foo"; // NaN
typeof a === "number"; // true
“不是数字的数字”仍然是数字类型。
特殊性
NaN是一个特殊值,它和自身不相等。
let a = 2 / "foo";
a == NaN; // false
a === NaN; // false
可以使用内置的全局工具函数
isNaN(..)
来判断一个值是否是NaN
。
注意:
isNaN(…)方法只有接受的参数是一个number类型
才会得到“合理”
的结果。
例如:
let a = 2 / "foo"; // 带有强制类型转换, "foo" 会被强制类型转换为 number 类型
console.log(Number("foo")); // NaN
// 实际为 var = 2 / NaN ;
let b = "foo";
a; // NaN
b; "foo"
window.isNaN(a); // true
// 给 isNaN() 传入了 string 类型,而非 number 类型
window.isNaN(b); // true
💕最稳妥的方法: 💕
ECMAScript 2015 中定义的 Number.isNaN()
来判断。它是原来的全局 isNaN()
的更稳妥的版本。
let a = 2 / "foo"; // 带有强制类型转换 "foo" 会被强制类型转换为 number 类型
let b = "foo";
console.log(Number.isNaN(a)); // true
console.log(Number.isNaN(b)); // false
+0 与 -0
JavaScript有一个常规的0(也叫作+0)和一个-0
对负零进行字符串化会返回"0"
let a = 0 / -3;
// 至少在某些浏览器的控制台中显示是正确的
console.log(a); // -0
// 但是规范定义的返回结果是这样!
console.log(a.toString()); // "0"
console.log(a + ""); // "0"
console.log(String(a)); // "0"
// JSON也如此
console.log(JSON.stringify(a)); // "0"
当字符串 "-0"
转换为number 类型
时会将其转换为 number 类型的 -0
。
注意:
JSON.stringify(-0)
返回"0"
,而JSON.parse("-0")
返回-0
。
+0 与 -0的比较操作
-0 == 0; // true
-0 === 0; // true
0 > -0; // false
0 < -0; // false
无论是宽松相等还是严格相等,+0 与 -0 都相等。
对于 NaN 不等于 NaN
,+0 等于 -0
这种情况,在ES6中新加入了一个工具方法Object.is(..)
来判断两个值是否绝对
相等(严格相等)。
let a = 2 / "foo"; // NaN
let b = -3 * 0; // -0
Object.is(a, NaN); // true
Object.is(b, -0); // true
Object.is(b, 0); // false
能使用
==
或===
时应避免使用 Object.is()
方法,因为前者效率更高。
原生函数
JavaScript的内建函数,也叫原生函数。
常用的原生函数有: • String()
• Number()
• Boolean()
• Array()
• Object()
• Function()
• RegExp()
• Date()
• Error()
• Symbol()——ES6中新加入的!
原生函数可以被当作构造函数来使用
let a = new String("abc");
typeof a; // 是"object",不是"String"
// 原型链判断
a instanceof String; // true
Object.prototype.toString.call(a); // "[object String]"
通过构造函数(如new String(“abc”))创建出来的是
封装了基本类型值
(如"abc")的封装对象
内部属性[[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]"
上例中,数组的内部[[Class]]属性值是"Array"
,正则表达式的值是"RegExp"。多数
情况下,对象的内部[[Class]]属性
和创建该对象的内建原生构造函数相对应
,但并非总是如此。
Object.prototype.toString.call(null);
// "[object Null]"
Object.prototype.toString.call(undefined);
// "[object Undefined]"
虽然Null()
和Undefined()
这样的原生构造函数并不存在
,但是内部[[Class]]属性值仍然是"Null"
和"Undefined"
。
Object.prototype.toString.call("abc");
// "[object String]"
Object.prototype.toString.call(42);
// "[object Number]"
Object.prototype.toString.call(true);
// "[object Boolean]"
基本类型值
被各自的封装对象自动包装
,所以它们的内部[[Class]]属性值分为"String"、“Number"和"Boolean”
封装对象包装
由于基本类型值没有.length
和.toString()
这样的属性和方法,需要通过封装对象才能访问,此时JavaScript
会自动
为基本类型值包装
(box或者wrap)一个封装对象。
let a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
let a = null;
let b = Object(a); // 和Object()一样 ---> 空对象
let c = undefined;
let d = Object(c); // 和Object()一样 ---> 空对象
因为没有对应的封装对象,所以null和undefined不能够被封装(boxed), Object(null)和Object()均返回一个常规对象(空对象)。
拆封
如果想要得到封装对象
中的基本类型值
,可以使用valueOf()
函数
let a = new String("abc");
let b = new Number(42);
let c = new Boolean(true);
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
在需要用到封装对象中的基本类型值的地方会发生隐式拆封
(强制类型转换)
let a = new String("abc");
// a 会进行 ToPrimitive 的强制类型转换
let b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"
原生函数作为构造函数
Array(…)
构造函数Array(…)
不要求必须带new关键字
。不带时,它会被自动补上
。因此Array(1,2,3)和new Array(1,2,3)的效果是一样的。
Array构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。这样创建出来的只是一个空数组,只不过它的length属性被设置成了指定的值。
let a = new Array(3);
console.log(a.length); // 3
console.log(a); // (3)[空属性 x 3]
console.log(a[0]); // undefined
// 这意味着它有三个值为undefined的单元,但实际上单元并不存在
RegExp(…)
建议使用常量形式
(如/^a*b+/g)来定义正则表达式,这样不仅语法简单,执行效率也更高,因为JavaScript引擎
在代码执行前会
对它们进行预编译
和缓存
。
let name = "Kyle";
let namePattern = new RegExp("\b(? :" + name + ")+\b", "ig");
Date(…)
创建日期对象必须使用new Date()
。其可以带参数,用来指定日期和时间,而不带参数
的话则使用当前的日期和时间
。如果调用Date()时不带new关键字,则会得到当前日期的字符串值。
const date = new Date();
console.log(date);
console.log(typeof(date)); // "object"
console.log(Date());
console.log(typeof(Date())); // "string"
强制类型转换
将值从一种类型转换为另一种类型
抽象操作ToString
处理非字符串到字符串的强制类型转换。
基本类型值的字符串化规则为:null转换为"null", undefined转换为"undefined", true转换为"true"。
对普通对象
来说,除非自行定义(部分对象会对toString方法进行重写),否则toString()(Object.prototype.toString())返回内部属性[[Class]]的值,如"[object Object]"
。
ES6
允许
从符号到字符串
的显式强制类型转换
,然而隐式强制类型转换会产生错误
let s1 = Symbol("cool");
String(s1); // "Symbol(cool)"
let s2 = Symbol("not cool");
s2 + ""; // TypeError
JSON字符串化
JSON.stringify(…)在将JSON对象序列化
为字符串时也用到了ToString,序列化的结果总是字符串😂
JSON.stringify(..)并不是强制类型转换。
JSON.stringify(42); // "42"
JSON.stringify("42"); // ""42""(含有双引号的字符串)
JSON.stringify(null); // "null"
JSON.stringify(true); // "true"
被JSON.stringify(…)所忽略的值:
对象中
遇到undefined
、function
和symbol
时会自动将其忽略
,在数组
中则会返回null
(以保证单元位置不变)
JSON.stringify(undefined); // undefined
JSON.stringify(function () { }); // undefined
JSON.stringify(Symbol("syh")); // undefined
// ---------------------------------------------------------------------
JSON.stringify(
[1, undefined, function () { }, 4, Symbol("syh")]
); // "[1,null,null,4,null]"
JSON.stringify(
{ a: 2, b: function () { }, c: Symbol("syh"), d: undefined }
); // '{"a":2}'
这也是为什么在
深拷贝
时,JSON.parse(JSON.stringify())
方式的不保险的原因所在
。如果是类型已知且确定没有undefined、function和symbol时,使用这种方式处理才比较保险。
JSON.stringify(…)的可选参数replacer
replacer是一个数组
如果replacer是一个数组,那么它必须是一个字符串数组
,其中包含序列化要处理的对象的属性名称
,除此之外其他的属性则被忽略
。
let a = {
b: 42,
c: "42",
d: [1, 2, 3]
};
// 只对属性 b、c 进行序列化,忽略 d 属性
console.log(JSON.stringify(a, ["b", "c"])); // '{"b":42, "c":"42"}'
replacer是一个函数
如果replacer是一个函数,它会对对象本身调用一次
,然后
对对象中
的每个属性各调用一次
,每次传递两个参数,键
和值
,如果要忽略某个键就返回undefined,否则返回指定的值。
let a = {
b: 42,
c: "42",
d: [1, 2, 3]
};
JSON.stringify(a, function (k, v) {
// k ==> 属性名
// v ==> 属性值
if (k !== "c") return v;
});
// '{"b":42, "d":[1,2,3]}'
当
第一次调用 JSON.stringify
时(就是对对象本身调用的那次),传递给 replacer 函数的参数k
是undefined
,而参数v
则是要序列化的整个对象a
。这是因为在第一次调用 replacer 函数时,它处理的是最外层的对象。
由于
JSON 字符串化是递归的
😎,这意味着它会递归地处理嵌套在对象或数组中的其他对象和数组
,以将整个数据结构转换为 JSON 字符串,因此数组[1,2,3]
中的每个元素
都会通过参数v
传递给replacer
,即1、2和3,参数k是它们的索引值
,即0、1和2。
抽象操作ToNumber
将非数字值当作数字来使用
其中true转换为1
, false转换为0
。undefined转换为NaN
, null转换为0
。
“”、“\n”(或者" "等其他空格组合)等空字符串被ToNumber强制类型转换为0。
抽象操作ToPrimitive
对象(包括数组
)会首先被转换为
相应的基本类型值
,而转换为相应基本类型值的方法便是抽象操作ToPrimitive
ToPrimitive步骤:
首先
(通过内部操作[[DefaultValue]]
)检查该值是否有valueOf()
方法(封装对象的拆封
操作)。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString()
的返回值(如果存在)来进行强制类型转换。
如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误。
从ES5开始,使用
Object.create(null)
创建的对象(“真空对象”)[[Prototype]]属性为null
,并且没有valueOf()和toString()方法
,因此无法进行强制类型转换
let a = {
valueOf: function () {
return "42";
}
};
let b = {
toString: function () {
return "42";
}
};
let c = [4, 2];
c.toString = function () {
// join("") 中空字符串相当于 ==> "4" + "2"
//join("-") ==> "4-2"
return this.join(""); // "42"
};
// 调用 valueOf() 方法
Number(a); // 42
// 首先调用 valueOf() 方法,发现没有,则调用 toString() 方法
Number(b); // 42
// 重写了数组本身的 toString 方法
Number(c); // 42
Number(""); // 0
// "" 强转 ==> 0
Number([]); // 0
// 调用数组的 toString 方法 ==> "abc"
// 对 "abc" 进行 number 类型强转 ==> NaN
Number(["abc"]); // NaN
注意: Array 本身没有 valueOf 方法。
Array(1,23).__proto__.hasOwnProperty("valueOf"); // false
hasOwnProperty() 方法不对原型链进行检查,只会检查对象自身的属性。
in
操作符可以用于检查属性是否存在于对象本身或其原型链中。
如果只想检查对象自身是否具有属性,使用
hasOwnProperty()
方法是更合适的。如果希望检查对象的整个原型链
,包括继承的属性,使用in
操作符。
ToBoolean
被强制类型转换为布尔值。
假值
•undefined • null • false • +0、-0和NaN • “”
假值列表以外的值都是真值。
字符串和数字之间的转换
字符串和数字之间的转换是通过String(..)
和Number(..)
这两个内建函数
来实现的,请注意它们前面没有new关键字
,并不创建封装对象
。
let a = 42;
let b = String(a);
let c = "3.14";
let d = Number(c);
b; // "42"
d; // 3.14
a.toString()
是显式的,不过其中涉及隐式转换。因为toString()
对42这样的基本类型值不适用,所以JavaScript引擎会自动为42创建一个封装对象
,然后对该对象调用toString()
。这里显式转换中含有隐式转换。
let a = 42;
let b = a.toString();
b; // "42"
let c = "3.14";
let d = +c;
d; // 3.14
+c
是+运算符的一元(unary)形式
(即只有一个操作数)。+运算符显式地将c转换为数字
,而非数字加法运算。
显式转换为布尔值
let a = "0";
let b = [];
let c = {};
let d = "";
let e = 0;
let f = null;
let g;
Boolean(a); // true
Boolean(b); // true
Boolean(c); // true
Boolean(d); // false
Boolean(e); // false
Boolean(f); // false
Boolean(g); // false
方法二:
显式强制类型转换为布尔值最常用的方法是
!!
,因为第二个!会将结果反转回原值
let a = "0";
let b = [];
let c = {};
let d = "";
let e = 0;
let f = null;
let g;
!!a; // true
!!b; // true
!!c; // true
!!d; // false
!!e; // false
!!f; // false
!!g; // false
字符串和数字之间的隐式强制类型转换
如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,该抽象操作再调用[[DefaultValue]]
,以数字作为上下文。
let a = "42";
let b = "0";
let c = 42;
let d = 0;
// 操作数都是字符串,所以进行的是字符串拼接
a + b; // "420"
// c,d 是 number 类型,本身就是原始值
c + d; // 42
JavaScript 中的 [[DefaultValue]]
规范是 JavaScript 引擎内部的一个抽象操作,用于确定如何将一个值转换为原始值(primitive value)。
[[DefaultValue]]
规范包括两个参数,一个是 hint
,另一个是 preferredType
。
hint
表示希望将对象
转换为什么类型的原始值,可以是 string 或 number。preferredType
表示首选的类型,如果hint
无法确定时会使用。
根据这两个参数的不同组合,[[DefaultValue]]
规范会执行不同的转换操作。以下是一些示例:
- 如果
hint
和preferredType
都未指定,[[DefaultValue]]
将首先尝试转换为字符串(preferredType 为 “string”),如果无法成功,然后再尝试转换为数字。 - 如果
hint
为 string,则[[DefaultValue]]
会尝试调用对象的toString()
方法来获取字符串表示。如果没有toString()
方法,它将尝试调用valueOf()
方法。 - 如果
hint
为 number,则[[DefaultValue]]
会尝试调用对象的valueOf()
方法来获取数值表示。如果没有valueOf()
方法,它将尝试调用toString()
方法。 - 如果
hint
和preferredType
均指定,并且指定的方法返回的结果不是合法的原始值,则引发 TypeError。
preferredType
参数在实际应用中使用得相对较少,通常默认情况下不需要显式指定它,JavaScript 会根据hint
来执行适当的转换。
let a = [1, 2];
let b = [3, 4];
// a,b 操作数都是数组,在JavaScript中数组也是对象的一种
// 进行 ToPrimitive 抽象操作
a + b; // "1,23,4"
|| 和 &&
逻辑运算符||(或)
和&&(与)
或者称它们为“选择器运算符”
(selector operators)或者“操作数选择器运算符”
(operand selector operators)
在JavaScript中它们
返回的并不是布尔值
。它们的返回值是两个操作数中的一个
(且仅一个)。即选择两个操作数中的一个,然后返回它的值。
let a = 42;
let b = "abc";
let c = null;
a || b; // 42
a && b; // "abc"
c || b; // "abc"
c && b; // null
||
和&&
首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值
就先进行ToBoolean强制类型转换
,然后再执行条件判断。
逻辑或:
对于||
来说,如果左操作数判断结果为true
就返回第一个操作数(a和c)的值
,如果为false
就返回第二个操作数(b)的值
。
逻辑与:
&&
则相反,如果左操作数判断结果为true就返回第二个操作数(b)的值
,如果为false
就返回第一个操作数(a和c)的值
。
|| 与 && 的短路机制
对&&
和||
来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。即执行最短路径
。
以a && b
为例,如果a
是一个假值,足以决定&&
的结果,就没有必要再判断b
的值。同样对于a || b
,如果a
是一个真值,也足以决定||
的结果,也就没有必要再判断b
的值。
运算符优先级
&&
运算符的优先级高于||
,而||
的优先级又高于? :
宽松相等和严格相等
宽松相等
(loose equals)==
和严格相等
(strict equals)===
都用来判断两个值是否“相等”。==
允许在相等比较中进行强制类型转换
,而===
不允许。就从结论上来说“==
检查值是否相等
,===
检查值和类型是否相等
”。
对象(包括函数和数组)的宽松相等 ==。两个对象指向同一个值时即视为相等,
不发生强制类型转换
。
字符串和数字之间的相等比较
let a = 42;
let b = "42";
a === b; // false
a == b; // true
a == b是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行
强制类型转换
。
(1) 如果Type(x)是数字,Type(y)是字符串,则返回 x == ToNumber(y)的结果。
(2) 如果Type(x)是字符串,Type(y)是数字,则返回ToNumber(x) == y的结果。
进行
==
比较时,如果一个是数字,一个是字符串
,那么字符串
会被强制转换为数字
,使用toNumber
规则转换。
其他类型和布尔类型之间的相等比较
let x = true;
let y = "42";
x == y; // false
Type(x)是布尔值,所以ToNumber(x)将true强制类型转换为1,变成1 == “42”,二者的类型仍然不同,"42"根据规则被强制类型转换为42,最后变成1 == 42,结果为false。
(1) 如果Type(x)是布尔类型,则返回ToNumber(x) == y的结果;
(2) 如果Type(y)是布尔类型,则返回x == ToNumber(y)的结果。
null和undefined之间的相等比较
let a = null;
let b; // undefined
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
(1) 如果x为null, y为undefined,则结果为true。
(2) 如果x为undefined, y为null,则结果为true。
在 == 中,null与undefined互相相等,也就是说可以相互进行隐式强制类型转换,且等于自身。但不与其他任何值相等。比如null不等于false
对象和非对象之间的相等比较
let a = 42;
let b = [42];
a == b; // true
(1) 如果Type(x)是字符串或数字,Type(y)是对象,则返回x == ToPrimitive(y)的结果;
(2) 如果Type(x)是对象,Type(y)是字符串或数字,则返回ToPrimitive(x) == y的结果。
自动分号插入(ASI)
有时JavaScript会自动为代码行补上缺失的分号
,即自动分号插入
(Automatic Semicolon Insertion,ASI
)
ASI
只在换行符
处起作用,而不会在代码行的中间插入分号
如果JavaScript解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分号
。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时
,它才会这样做。
暂时性死区(TDZ)
TDZ
指的是由于代码中的变量还没有初始化而不能被引用
的情况。这个错误发生在变量被声明之后,但在赋值之前的阶段。
{
// 未捕获的ReferenceError:初始化前无法访问“a”
a = 2; // Uncaught ReferenceError: Cannot access 'a' before initialization
let a;
}
{
typeof a; // undefined
typeof b; // ReferenceError! (TDZ)
let b;
}
注意:
使用
var
声明的变量不会进入暂时性死区,当使用var
声明变量时,变量会被提升(hoisting),这意味着变量的声明会在代码执行之前被移到当前作用域的顶部。这使得你可以在声明之前引用var
变量而不会引发错误。然而,如果你在声明之前访问var
变量,它会返回undefined
。
console.log(x); // 不会抛出错误,但会打印 "undefined"
// var 声明的变量会存在变量提升
var x = 42; // 声明并初始化变量
console.log(x); // 打印 "42"
try…finally
finally
中的代码总是会在try
之后执行,如果有catch
的话则在catch
之后执行。也可以将finally
中的代码看作一个回调函数
,即无论出现什么情况最后一定会被调用。
在try中出现return的情况
function foo() {
try {
return 42;
}
finally {
console.log("Hello");
}
console.log("never runs");
}
console.log(foo());
// Hello
// 42
return 42先执行但是暂时不返回!
,并将foo()
函数的返回值设置为42
。然后try
执行完毕,接着执行finally
。最后foo()
函数执行完毕,console.log(…)显示返回值。
在try中出抛出异常的情况
function foo() {
try {
throw 42;
}
finally {
console.log("Hello");
}
console.log("never runs");
}
console.log(foo());
// Hello
// Uncaught Exception: 42
如果finally中抛出异常
(无论是有意还是无意),函数
就会在此处终止
。如果此前try
中已经有return
设置了返回值,则该值会被丢弃
function foo() {
try {
// 被丢弃!!
return 42;
}
finally {
throw "Oops! ";
}
console.log("never runs");
}
console.log(foo());
// Uncaught Exception: Oops!
在finally中出现return的情况
finally中的return会覆盖try和catch中return的返回值
function baz() {
try {
return 42;
}
finally {
// 覆盖前面的return 42
// 只要出现 return 就覆盖 try 中的 return
return "Hello";
}
}
baz(); // "Hello"
switch
switch (a) {
case 2:
// 执行一些代码
break;
case 42:
// 执行另外一些代码
break;
default:
// 执行缺省代码
}
a
和case
表达式的匹配算法与 ===
相同(严格相等)。
<script>
标签
通常可以在网页中使用<script src=..></script>
来加载这些文件,或者使用<script> .. </script>
来包含内联代码(inline-code),而且它们共享global对象
(在浏览器中则是window
),也就是说这些文件中的代码在共享的命名空间中运行,并相互交互
。
<script>foo(); </script>
<script>
function foo() { ..}
</script>
在第一个script
中定义了函数foo()
,后面的script
代码就可以访问并调用foo()
,就像foo()在其内部被声明过一样。
但是全局变量作用域的提升机制(hoisting)在这些边界中不适用,因此无论是<script> .. </script>
还是<script src=..></script>
,简单的来说就是只会在自己的script标签中进行变量提升
,自己的script标签便是边界。
<script>
// 函数会变量提升
foo();
function foo() { .. }
</script>
但是即便是提升,也不会越过自己的边界:
<script>foo(); </script>
<script>
function foo() { .. }
</script>
若更改script的顺序则可以运行,如:
<script>
function foo() { .. }
</script>
<script>foo(); </script>
如果
script
中的代码(无论是内联代码还是外部代码)发生错误
,它会像独立的JavaScript程序那样停止
,但是后续的script中的代码
(仍然共享global)依然会接着运行,不会受影响
。
参考资料: 《你不知道的JavaScript中卷》