你不知道的JavaScript(中卷) 第三、四章

Jiutocrx 2019-09-05

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


第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有意思

相关推荐