你不知道的JavaScript中卷-第三四章

49次阅读

共计 7908 个字符,预计需要花费 20 分钟才能阅读完成。

这里的内容是读书笔记,仅供自己学习所用,有欠缺的地方欢迎留言提示。


第 3 章 原生函数
JavaScript 的内建函数,也叫原生函数,如 String 和 Number。
常用的原生函数有:

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

实际上,它们就是内建函数。
原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和我们设想的有所出入。

let a = new String('abc');
typeof a; // 是 "object",不是 "string"
a instanceof String; // true
Object.prototype.toString.call(a); // "[object String]"

通过构造函数创建出来的是封装了基本类型值(如 ”abc”)的封装对象。
请注意:typeof 在这里返回的是对象类型的子类型。
再次强调,new String(“abc”)创建的是字符串 ”abc” 的封装对象,而非基本类型值 ”abc”。

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

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

多数情况下,对象的内部 [[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]"

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

let a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

如果需要经常用到这些字符串属性和方法,那么从一开始就创建一个封装对象也许更为方便,这样 JavaScript 引擎就不用每次都自动创建了。但实际证明这并不是一个好办法,因为浏览器已经为.length 这样的常见情况做了性能优化,直接使用封装对象来“提前优化”代码反而会减低执行效率。
一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript 引擎自己决定什么时候应该使用封装对象。
tip: 优先考虑使用基本类型值。

封装对象释疑
使用封装对象时有些地方需要特别注意。

let a = new Boolean(false);
if(!a) {console.log('here'); // 执行不到这里
}

3.3 拆封
如果想要得到封装对象中的基本类型,可以使用 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

3.4 原生函数作为构造函数
关于数组(array)、对象(object)、函数(function)和正则表达式,我们通常喜欢以常量的形式来创建它们。实际上,使用常量和是同构造函数的效果是一样的(创建的值都是通过封装对象来包装)。
如前所述,应该尽量避免使用构造函数,除非十分必要,因为它们经常会产生意想不到的结果。

3.4.1 Array(..)
Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。
着实非明智之举:一是容易忘记,二是容易出错。
更为关键的是,数组并没有预设长度这个概念。这样创建出来的只是一个空数组,只不过它的 length 属性被设置成了指定的值。

3.4.2 Object(..)、Function(..)和 RegExp(..)
同样,除非万不得已,否则尽量不要使用 Object(..)/Function(..)/RegExp(..)。

3.4.3 Date(..)和 Error(..)
相较于其他原生构造函数,Date(..)和 Error(..)的用处要大很多,因为没有对应的常量形式来作为它们的替代。
创建日期对象必须使用 new Date()。Date(..)可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。
Date(..)主要用来获得当前的 Unix 时间戳(从 1970 年 1 月 1 日开始计算,以秒为单位)。该值可以通过日期对象中的 getTime()来获得。
创建错误对象(error object)主要是为了获得当前运行栈的上下文(大部分 JavaScript 引擎通过只读属性.stack 来访问)。栈上下文信息包括函数调用栈信息和产生错误的代码行号,以便于调式(debug)。
错误对象通常与 throw 一起使用:

function Foo(x) {if(!x) {throw new Error("x wasn't provided");
    }
    // ..
}

通常所悟对象至少包含一个 message 属性,有时也不乏其他属性(必须作为只读属性访问)。

3.4.4 Symbol(..)
ES6 中新加入了一个基本数据类型——符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。

obj[Symbol.iterator] = function() { /*..*/};

符号并非对象,而是一种简单标量基本类型。

3.4.5 原生原型
原生构造函数有自己的.prototype 对象,如 Array.prototype、String.prototype 等。
这些对象包含其对应子类型所特有的行为特征。
例如,将字符串值封装为字符串对象之后,就能访问 String.prototype 中定义的方法。
根据文档约定,我们将 String.prototype.XYZ 简写为 String#XYZ,对其他.prototype 也同样如此。

  • String#indexOf(..)
    在字符串中找到指定子字符串的位置。
  • String#charAt(..)
    获得字符串指定位置上的字符。
  • String#substr(..)、String#substring(..)和 String#slice(..)
    获得字符串的指定部分。
  • String#toUpperCase()和 String#toLowerCase()
    将字符串转换为大写或小写
  • String#trim()
    去掉字符串前后的空格,返回新的字符串。

以上方法并不改变原字符串的值,而是返回一个新字符串。
tip: trim 可以用来校验是否为空字符串,但是 trim 只能去掉字符串前后的空格,字符之间夹杂的空格并不饿能去掉。

typeof Function.prototype; // "function"
Function.prototype(); // 空函数!RegExp.prototype.toString(); // "/(?:)/"——空正则表达式
"abc".match(RegExp.prototype); // [""]

Array.isArray(Array.prototype); // true

Function.prototype 是一个函数,RegExp.prototype 是一个空的正则表达式,而 Array.prototype 是一个空数组。这里,将原型作为默认值。

tips: 从 ES6 开始,我们不再需要使用 vals = vals || .. 这样的方式来设置默认值,因为默认值可以通过函数声明中的内置语法来设置。

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

第 4 章 强制类型转换

4.1 值类型转换
将值从一种类型转换为另一种类型通常称为类型转换,这是显式的情况;隐式的情况称为强制类型转换。
也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。
然而在 JavaScript 中通常将它们统称为强制类型转换。
JavaScript 中的强制类型转换总是返回标量基本类型值,如字符串、数字和布尔值,不会返回对象和函数。

4.2 抽象值操作

4.2.1 ToString
toString 负责处理非字符串到字符串的强制类型转换。
tips: 基本类型值的字符串化规则为:null 转换为 ”null”,undefined 转换为 ”undefined”,true 转换为 ”true”。数字的字符串化规则遵循通用规则。
如果对象有自己的 toString()方法,字符串化时就会调用该方法并使用其返回值。
JSON 字符串化
工具函数 JSON.stringify(..)在将 JSON 对象序列化为字符串时也用到了 ToString。
所有安全的 JSON 值都可以使用 JSON.stringify(..)字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。
undefined、function、symbol 和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。
JSON.stringify(..)在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。

JSON.stringify(undefined); // undefined
JSON.stringify(function () {}); // undefined
JSON>stringify([1, undefined, function() {}, 4]
); // "[1, null, null, 4]"
JSON.stringify({ a: 2, b: function() {}}
); // "{"a": 2}"

如果对象中定义了 toJSON()防范,JSON 字符串化时会首先调用该方法,然后用它的返回值进行序列化。
很多人误以为 toJSON()返回的是 JSON 字符串化后的值,其实不然,除非我们确实想要对字符串进行字符串化(通常不会!)。toJSON()返回的应该是一个适当的值,可以是任何类型,然后再由 JSON.stringify(..)对其进行字符串化。也就是说,toJSON()应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回一个 JSON 字符串”。
JSON.stringify(..)并不是强制类型转换,涉及 ToString 强制类型转换,具体表现在以下两点。
(1)字符串、数字、布尔值和 null 的 JSON.stringify(..)规则与 ToString 基本相同。
(2)如果传递给 JSON.stringify(..)的对象中定义了 toJSON()方法,那么该方法会在字符串化前调用,以便将对象转换为安全的 JSON 值。

4.2.2 ToNumber
ES5 定义了抽象操作 ToNumber。
tips:其中 true 转换为 1,false 转换为 0,undefined 转换为 NaN,null 转换为 0。
ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法,处理失败时返回 NaN。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先检查该值是否有 valueOf()方法;如果有并且返回基本类型值,就使用该值进行强制类型转换,如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换。
如果 valueOf()和 toString()均不返回基本类型值,会产生 TypeError 错误。

let c = [4, 2];
c.toString = function() {return this.join(""); //"42"
}
Number(c); // 42
Number(""); // 0
Number([]); // 0
Number(["abc"]); // NaN

4.2.3 ToBoolean
1、假值
JavaScript 中的值可以分为以下两类:
(1)可以被强制类型转换为 false 的值;
(2)其他(被强制类型转换为 true 的值)。
JavaScript 规范具体定义了一小撮可以被强制类型转换为 false 的值。
tips:以下这些是假值:

  • undefined
  • null
  • false
  • +0、- 0 和 NaN
  • “”

假值的布尔强制类型转换结果为 false。

2、假值对象

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

let a = "false";
let b = "0";
let c = "''";

let aa = [];
let bb = {};
let cc = function() {};

let d = Boolean(a && b && c && aa && bb && cc);
d; // true

以上的值都不在假值列表中,都是真值,不过 ”” 除外,因为它是假值列表中唯一的字符串。
也就是说真值列表可以无限长,无法一一列举,所以我们只能用假值列表作为参考。

4.3 显式强制类型转换

4.3.1 字符串和数字之间的显式转换
1、日期显式转换为数字
tips: 建议使用 Date.now() 来获得当前的时间戳,使用 new Date(..).getTime()来获得指定时间的时间戳。

2、奇特的~ 运算符
按照离散数学来解释:~ 返回 2 的补码;也就是说~x 等同于 -(x+1)。

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

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

= 0 和 ==- 1 这样的写法不是很好,称为“抽象渗漏”,意思是在代码中莫楼了底层的实现细节,这里只用 - 1 作为失败时的返回值,这些细节应该被屏蔽掉。

if(~a.indexOf("lo")) { // true
    // 找到匹配!}

4.3.2 显式解析数字字符串
解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。

let a = "42";
let b = "42px";
Number(a); // 42
parseInt(a); // 42

Number(b); // NaN
parseInt(b); // 42

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

4.3.3 显式转换为布尔值
布尔值情况:

  • “” false
  • 0 false
  • null false
  • undefined false

显式强制类型转换为布尔值最常用的方法是!!。

4.4 隐式强制类型转换
隐式强制类型转换值得是那些隐蔽的强制类型转换,副作用也不是很明显。
隐式强制类型转换的作用是减少冗余,让代码更简洁。

4.4.1 隐式地简化
隐式强制类型转换同样可以用来提高代码可读性。然而隐式强制类型转换也会带来一些负面影响,有时甚至是弊大于利,但是不应该“因噎废食”。

4.4.2 字符串和数字之间的隐式强制类型转换
+ 运算符即能用于数字加法,也能用于字符串拼接。

let a = [1, 2];
let b = [3, 4];
a + b; // "1, 23, 4"

操作数的 valueOf()操作无法得到简单基本类型值,于是转而调用 toString()。
tips: a+”” 可以转换为字符串;a- 0 可以转换为数字。

4.4.3 布尔值到数字的隐式强制类型转换
4.4.6 符号的强制类型转化
ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。

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

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

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)。

4.5 宽松相等和严格相等
== 允许在相等比较中进行强制类型转换,而 === 不允许。

4.5.1 相等比较操作的性能
如果两个值的类型不同,需要考虑有没有强制类型转换的必要,有就用 ==,没有就用 ===,不用在乎性能。
== 和 === 都会检查操作数的类型,区别在于操作数类型不同时它们的处理方式不同。

4.5.2 抽象相等
需要注意的是:

  • NaN 不等于 NaN
  • +0 等于 -0

对象(包括函数和数组)的宽松相等 ==。两个对象指向同一个值时即视为相等,不发生强制类型转换。
在比较两个对象的时候,== 和 === 的工作原理是一样的。

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

let a = '42';
let b = true;
a == b; // false

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

3、null 和 undefined 之间的相等比较
规范:
(1)如果 x 为 null,y 为 undefined,则结果为 true;
(2)如果 x 为 undefined,y 为 null,则结果为 true。
在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。

4.5.3 比较少见的情况

'0' == false;
false == 0;
false == '';
false == [];
''== 0;'' == [];
0 == [];

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

4.6 抽象关系比较
奇奇怪怪的东西???

let a = {b: 42};
let b = {b: 42};
a < b; // false
a == b; // false
a > b; // false

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

JavaScript 中 <= 是“不大于”的意思。比如:a <= b 就是 a >b 的反转。emmmm 有意思

正文完
 0