《你不知道的JavaScript(中卷)》

前言

本文作为对本书的一些知识点的收集

正文

1. 类型

ECMAScript 语言类型包括 Undefined、Null、Boolean、String、Number 和 Object。

  1. 内置类型
  • JavaScript 有七种内置类型:
    • 空值(null)
    • 未定义(undefined)
    • 布尔值( boolean)
    • 数字(number)
    • 字符串(string)
    • 对象(object)
    • 符号(symbol,ES6 中新增)

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

  1. 小结

JavaScript 有 七 种 内 置 类 型:nullundefinedbooleannumberstringobjectsymbol,可以使用 typeof 运算符来查看。

变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
很多开发人员将 undefinedundeclared 混为一谈,但在 JavaScript 中它们是两码事。

undefined 是值的一种。undeclared 则表示变量还没有被声明过。
遗憾的是,JavaScript 却将它们混为一谈,在我们试图访问 "undeclared" 变量时这样报错:ReferenceError: a is not defined,并且 typeofundefinedundeclared 变量都返回 "undefined"。

然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的办法。

2. 值

  1. 数组

和其他强类型语言不同,在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的):

使用 delete 运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的 length 属性并不会发生变化。

  • 在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:
var a = [ ];
a[0] = 1;
// 此处没有设置a[1]单元
a[2] = [ 3 ];
a[1]; // undefined
a.length; // 3

上面的代码可以正常运行,但其中的“空白单元”(empty slot)可能会导致出人意料的结果。a[1] 的值为 undefined,但这与将其显式赋值为 undefineda[1] = undefined)还是有所区别。

  • 数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内):
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

在数组中加入字符串键值 / 属性并不是一个好主意。建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。

  1. 类数组
  • 有时需要将类数组(一组通过数字索引的值)转换为真正的数组
function foo() {
  var arr = Array.prototype.slice.call( arguments );
  arr.push( "bam" );
  console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]

如上所示,slice() 返回参数列表(上例中是一个类数组)的一个数组复本。

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

Array.from(..) 还有一些其他非常强大的功能

  1. 数字
  • 应该怎样来判断 0.1 + 0.20.3 是否相等呢?

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

ES6 开始,该值定义在 Number.EPSILON 中,我们可以直接拿来用,也可以为 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
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

能够呈现的最大浮点数大约是 1.798e+308(这是一个相当大的数字),它定义在 Number.MAX_VALUE 中。最小浮点数定义在 Number.MIN_VALUE 中,大约是 5e-324,它不是负数,但无限接近于 0 !

  • 整数检测

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

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

也可以为 ES6 之前的版本 polyfill Number.isInteger(..) 方法:

if (!Number.isInteger) {
  Number.isInteger = function(num) {
    return typeof num == "number" && num % 1 == 0;
  };
}

要检测一个值是否是安全的整数,可以使用 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

可以为 ES6 之前的版本 polyfill Number.isSafeInteger(..) 方法:

if (!Number.isSafeInteger) {
  Number.isSafeInteger = function(num) {
    return Number.isInteger( num ) && Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
  };
}
  • 不是值的值

undefined 类型只有一个值,即 undefinednull 类型也只有一个值,即 null。它们的名称既是类型也是值。

undefinednull 常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:
null 指空值(empty value)
undefined 指没有值(missing value)
或者:
undefined 指从未赋值
null 指曾赋过值,但是目前没有值

null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而
undefined 却是一个标识符,可以被当作变量来使用和赋值。

  • 特殊的数字 - NaN

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

判断一个值是否是 NaN,从 ES6 开始我们可以使用工具函数 Number.isNaN(..)ES6 之前的浏览器的 polyfill 如下:

if (!Number.isNaN) {
  Number.isNaN = function(n) {
    return (
      typeof n === "number" && window.isNaN( n )
    );
  };
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false——好!
  • 特殊等式

如前所述,NaN-0 在相等比较时的表现有些特别。由于 NaN 和自身不相等,所以必须使用 ES6 中的 Number.isNaN(..)(或者 polyfill)。而 -0 等于 0(对于 === 也是如此,参见第4 章),因此我们必须使用 isNegZero(..) 这样的工具函数。
ES6 中新加入了一个工具方法 Object.is(..) 来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:

var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false

对于 ES6 之前的版本,Object.is(..) 有一个简单的 polyfill

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;
  };
}

能使用 =====(参见第 4 章)时就尽量不要使用 Object.is(..),因为前者效率更高、更为通用。Object.is(..) 主要用来处理那些特殊的相等比较。

  1. 值和引用

JavaScript 中没有指针,引用的工作机制也不尽相同。在 JavaScript 中变量不可能成为指向另一个变量的引用。

JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系。

JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

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

复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则总是通过引用复制的方式来赋值 / 传递。

  • 函数参数相关的代码示例
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]

我们向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x,而 a 仍然指向 [1,2,3]
在函数中我们可以通过引用 x 来更改数组的值(push(4) 之后变为 [1,2,3,4])。但 x = [4,5,6] 并不影响 a 的指向,所以 a 仍然指向 [1,2,3,4]
我们不能通过引用 x 来更改引用 a 的指向,只能更改 ax 共同指向的值。

  • 如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。
function foo(x) {
 x.push( 4 );
 x; // [1,2,3,4]
 // 然后
 x.length = 0; // 清空数组
 x.push( 4, 5, 6, 7 );
 x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]

从上例可以看出,x.length = 0x.push(4,5,6,7) 并没有创建一个新的数组,而是更改了当前的数组。于是 a 指向的值变成了 [4,5,6,7]
请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。

  1. 小结

JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的行为特征不同,在将字符作为数组来处理时需要特别小心。
JavaScript 中的数字包括“整数”和“浮点型”。

基本类型中定义了几个特殊的值。
null 类型只有一个值 nullundefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefinedvoid 运算符返回 undefined
数 字 类 型 有 几 个 特 殊 值, 包 括 NaN( 意 指 “not a number”, 更 确 切 地 说 是 “invalid number”)、+Infinity-Infinity-0

简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。
JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。

3. 原生函数

JavaScript 的内建函数(built-in function),也叫原生函数(native function),如 StringNumber

  1. 常用的原生函数有:
  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()——ES6 中新加入的!

实际上,它们就是内建函数。

  • 代码示例
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"

通过构造函数(如 new String("abc"))创建出来的是封装了基本类型值(如 "abc")的封装对象。
请注意:typeof 在这里返回的是对象(String)类型的子类型(object)。

  1. 将原型作为默认值

Function.prototype 是一个空函数,RegExp.prototype 是一个“空”的正则表达式(无任何匹配),而 Array.prototype 是一个空数组。对未赋值的变量来说,它们是很好的默认值。

function isThisCool(vals,fn,rx) {
  vals = vals || Array.prototype;
  fn = fn || Function.prototype;
  rx = rx || RegExp.prototype;
  return rx.test(
    vals.map( fn ).join( "" )
  );
}
isThisCool(); // true

isThisCool(
  ["a","b","c"],
  function(v){ return v.toUpperCase(); },
  /D/
); // false

这种方法的一个好处是 .prototypes 已被创建并且仅创建一次。相反,如果将 []
function(){}/(?:)/ 作为默认值,则每次调用 isThisCool(..) 时它们都会被创建一次(具体创建与否取决于 JavaScript 引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和 CPU 资源的浪费。
另外需要注意的一点是,如果默认值随后会被更改,那就不要使用 Array.prototype。上例中的 vals 是作为只读变量来使用,更改 vals 实际上就是更改 Array.prototype,而这样会导致前面提到过的一系列问题!

  1. 小结

JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 StringNumberBoolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String.prototype.trim()Array.prototype.concat(..))。

对于简单标量基本类型值,比如 "abc",如果要访问它的 length 属性或String.prototype 方法,JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。

4. 强制类型转换

  1. 值类型转换

也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在、动态类型语言的运行时(runtime)。

然而在 JavaScript 中通常将它们统称为强制类型转换,我个人则倾向于用“隐式强制类型转换”(implicit coercion)和“显式强制类型转换”(explicit coercion)来区分。

二者的区别显而易见:我们能够从代码中看出哪些地方是显式强制类型转换,而隐式强制类型转换则不那么明显,通常是某些操作产生的副作用。

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

对变量 b 而言,强制类型转换是隐式的;由于 + 运算符的其中一个操作数是字符串,所以是字符串拼接操作,结果是数字 42 被强制类型转换为相应的字符串 "42"
String(..) 则是将 a 显式强制类型转换为字符串。
两者都是将数字 42 转换为字符串 "42"。然而它们各自不同的处理方式成为了争论的焦点。

  1. 假值列表
  • undefined
  • null
  • false
  • +0、-0 和 NaN
  • ""
  1. 日期显式转换为数字

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

var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000

我们常用下面的方法来获得当前的时间戳,例如:

var timestamp = +new Date();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容