作为一名前端小白,不知道大家是否遇到和我一样的问题。看了一道面试题的解析,当时觉得会了,可是过两天以后再看又不会了;盲目追求各种新技术,感觉什么都会点,但是一上手就不行了... 痛定思痛后,我终于认识到了问题所在,开始专注于基本功的修炼。近半年来通读了(其实是囫囵吞枣)《JavaScript高级程序设计》、《你不知道的JavaScript上、中、下》等书籍,本系列文章是我读书过程中对知识点的一些总结。喜欢的同学记得帮我点个赞😁。
懂王系列(一)之彻底搞懂JavaScript函数执行机制
懂王系列(二)之彻底搞懂JavaScript作用域
懂王系列(三)之彻底搞懂JavaScript对象
懂王系列(四)之彻底搞懂JavaScript类
懂王系列(五)之彻底搞懂JavaScript原型
懂王系列(六)之彻底搞懂JavaScript中的this
懂王系列(七)之彻底搞懂JavaScript数据类型
懂王系列(八)之彻底搞懂JavaScript语句
懂王系列(九)之彻底搞定JavaScript类型转换
1. 值
JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值
1.1 数组
1.1.1 delete
使用 delete
运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的 length
属性并不会发生变化
1.1.2 稀疏数组
在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:
var a = [];
a[0] = 1;
// 此处没有设置a[1]单元
a[2] = [3];
a[1]; // undefined
a.length; // 3
上面的代码可以正常运行,但其中的“空白单元”(empty slot)可能会导致出人意料的结果。a[1]
的值虽然为 undefined
,但这与将其显式赋值为 undefined(a[1] = undefined)
是有区别的
1.1.3 数组键值
数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内)
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.1.4 类数组转为数组
有时需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如indexOf(..)、concat(..)、forEach(..) 等)来实现。
工具函数 slice(..) 经常被用于这类转换:
function foo() {
var arr = Array.prototype.slice.call(arguments);
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]
用 ES6 中的内置工具函数 Array.from(..) 也能实现同样的功能:
var arr = Array.from(arguments);
1.1.5 字符串
字符串经常被当成字符数组。字符串的内部实现究竟有没有使用数组并不好说,但 JavaScript 中的字符串和字符数组并不是一回事,最多只是看上去相似而已
var a = "foo";
var b = ["f","o","o"];
字符串和数组的确很相似,它们都是类数组,都有 length
属性以及 indexOf(..)
(从 ES5开始数组支持此方法)和 concat(..)
方法:
a.length; // 3
b.length; // 3
a.indexOf( "o" ); // 1
b.indexOf( "o" ); // 1
var c = a.concat( "bar" ); // "foobar"
var d = b.concat( ["b","a","r"] ); // ["f","o","o","b","a","r"]
a === c; // false
b === d; // false
a; // "foo"
b; // ["f","o","o"]
但这并不意味着它们都是“字符数组”,比如:
a[1] = "O";
b[1] = "O";
a; // "foo"
b; // ["f","O","o"]
JavaScript 中字符串是不可变的,而数组是可变的。并且 a[1]
在 JavaScript 中并非总是合法语法。正确的方法应该是 a.charAt(1)
。
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push( "!" );
b; // ["f","O","o","!"]
许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”数组的非变更方法来处理字符串
a.join; // undefined
a.map; // undefined
var c = Array.prototype.join.call(a, "-");
var d = Array.prototype.map.call(a, function(v){
return v.toUpperCase() + ".";
}).join( "" );
c; // "f-o-o"
d; // "F.O.O."
另一个不同点在于字符串反转(JavaScript 面试常见问题)。数组有一个字符串没有的可变更成员函数 reverse()
a.reverse; // undefined
b.reverse(); // ["!","o","O","f"]
b; // ["f","O","o","!"]
可惜我们无法“借用”数组的可变更成员函数,因为字符串是不可变的:
Array.prototype.reverse.call(a);
// 返回值仍然是字符串"foo"的一个封装对象
一个变通(破解)的办法是先将字符串转换为数组,待处理完后再将结果转换回字符串:
var c = a
// 将a的值转换为字符数组
.split("")
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接回字符串
.join("");
c; // "oof"
这种方法的确简单粗暴,但对简单的字符串却完全适用。
上述方法对于包含复杂字符(Unicode,如星号、多字节字符等)的字符串并不适用。这时则需要功能更加完备、能够处理 Unicode 的工具库。
如果需要经常以字符数组的方式来处理字符串的话,倒不如直接使用数组。这样就不用在字符串和数组之间来回折腾。可以在需要时使用 join("")
将字符数组转换为字符串。
1.2 数字
1.2.1 数字语法
JavaScript 中的“整数”就是没有小数的十进制数。所以 42.0
即等同于“整数”42
。
JavaScript 中的数字字面量一般用十进制表示。例如:
var a = 42;
var b = 42.3;
小数点前面的 0
可以省略:
var a = 0.42;
var b = .42;
小数点后小数部分最后面的 0
也可以省略:
var a = 42.0;
var b = 42.;
tofixed(..)
方法可指定小数部分的显示位数, toPrecision(..)
方法用来指定有效数位的显示位数
对于 .
运算符需要给予特别注意,因为它是一个有效的数字字符,会被优先识别为数字字面量的一部分,然后才是对象属性访问运算符。
// 无效语法:
42.toFixed( 3 ); // SyntaxError
// 下面的语法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42.toFixed(3)
是无效语法,因为 .
被视为常量 42.
的一部分,所以没有 .
属性访问运算符来调用 toFixed
方法。
42..toFixed(3)
则没有问题,因为第一个 .
被视为 number
的一部分,第二个 .
是属性访问运算符。只是这样看着奇怪,实际情况中也很少见。在基本类型值上直接调用的方法并不多见,不过这并不代表不好或不对。
下面的语法也是有效的(请注意其中的空格):
42 .toFixed(3); // "42.000"
然而对数字字面量而言,这样的语法很容易引起误会,不建议使用。
1.2.2 0.1+0.2问题
最常见的方法是设置一个误差范围值,通常称为“机器精度”(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) {
// abs(), 绝对值
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.2.3 整数检测
要检测一个值是否是整数,可以使用 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;
};
}
1.3 特殊值
1.3.1 null与undefined
• null 指空值(empty value)
• undefined 指没有值(missing value)
或者:
• undefined 指从未赋值
• null 指曾赋过值,但是目前没有值
已在作用域中声明但还没有赋值的变量,是 undefined
的。相反,还没有在作用域中声明过的变量,是 undeclared
(未定义) 的
null 是基本类型中唯一的一个“假值”,typeof对它的返回值为 "object",所以检测null类型需要通过如下方法:
var a = null;
(!a && typeof a === "object"); // true
1.3.2 void
表达式 void ___
没有返回值,因此返回结果是 undefined
。void
并不改变表达式的结果,只是让表达式不返回值:
var a = 42;
console.log(void a, a); // undefined 42
按惯例我们用 void 0
来获得 undefined
(这主要源自 C 语言,当然使用 void true 或其他 void 表达式也是可以的)。void 0
、void 1
和 undefined
之间并没有实质上的区别。
void
运算符在其他地方也能派上用场,比如不让表达式返回任何结果(即使其有副作用)。例如:
function doSomething() {
// 注: APP.ready 由程序自己定义
if (!APP.ready) {
// 稍后再试
return void setTimeout(doSomething, 100);
}
var result;
// 其他
return result;
}
// 现在可以了吗?
if (doSomething()) {
// 立即执行下一个任务
}
这里 setTimeout(..) 函数返回一个数值(计时器间隔的唯一标识符,用来取消计时),但是为了确保 if 语句不产生误报(false positive),我们要 void
掉它。
很多开发人员喜欢分开操作,效果都一样,只是没有使用 void
运算符:
if (!APP.ready) {
// 稍后再试
setTimeout( doSomething,100 );
return;
}
总之,如果要将代码中的值(如表达式的返回值)设为 undefined
,就可以使用 void
。这种做法并不多见,但在某些情况下却很有用。
1.3.3 特殊数字
- NaN
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
换句话说,“不是数字的数字”仍然是数字类型。
NaN
是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值
var b = "foo";
a; // NaN
b; "foo"
window.isNaN(a); // true
window.isNaN(b); // true——晕!
从 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
不等于自身这个特点。NaN
是 JavaScript 中唯一一个不等于自身的值。一一个不等于自身的值。于是我们可以这样:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
- 零值
有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位(sign)用来代表其他信息(比如移动的方向)。此时如果一个值为 0
的变量失去了它的符号位,它的方向信息就会丢失。所以保留 0
值的符号位可以防止这类情况发生。
var a = 0 / -3; // -0
var b = 0 * -3; // -0
乘除会得到负零,加法和减法运算不会得到负零(negative zero)。 对负零进行字符串化会返回 "0",JSON也如此
JSON.stringify(a); // "0"
但是其从字符串转换为数字,得到的结果是准确的:
+"-0"; // -0
Number("-0"); // -0
JSON.parse("-0"); // -0
1.3.4 特殊等式
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;
};
}
能使用 ==
和 ===
时就尽量不要使用 Object.is(..)
,因为前者效率更高
2. 原生函数
常用的原生函数:
• 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")的封装对象。
2.1 内部属性[[Class]]
所有 typeof
返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]]
(我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问,一般通过 Object.prototype.toString(..)
来查看
注意:函数是“可调用对象”,它有一个内部属性 [[Call]]
,该属性使其可以被调用
多数情况下,对象的内部 [[Class]]
属性和创建该对象的内建原生构造函数相对应(如下),但并非总是如此。
那么基本类型值呢?下面先来看看 null
和 undefined
:
Object.prototype.toString.call(null);
// "[object Null]"
Object.prototype.toString.call(undefined);
// "[object 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"
2.2 封装
如果想要自行封装基本类型值,可以使用 Object(..)
函数(不带 new 关键字):
var a = "abc";
var b = new String(a);
var c = Object(a);
typeof a; // "string"
typeof b; // "object"
typeof c; // "object"
b instanceof String; // true
c instanceof String; // true
Object.prototype.toString.call(b); // "[object String]"
Object.prototype.toString.call(c); // "[object String]"
再次强调,一般不推荐直接使用封装对象(如上例中的 b 和 c),但它们偶尔也会派上用场。
2.3 拆封
如果想要得到封装对象中的基本类型值,可以使用 valueOf()
函数:
var a = new String("abc");
var b = new Number(42);
var c = new Boolean(true);
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
2.4 原生函数作为构造函数
2.4.1 Array
构造函数 Array(..)
不要求必须带 new 关键字。不带时,它会被自动补上。因此 Array(1,2,3)
和 new Array(1,2,3)
的效果是一样的。
Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。
var a = new Array( 3 );
a.length; // 3
a;
a 在 Chrome 中显示为 [ empty x 3 ]
,稀疏数组
var a = new Array(3);
var b = [undefined, undefined, undefined];
var c = [];
c.length = 3;
a.join("-"); // "--"
b.join("-"); // "--"
a.map(function(v,i){ return i; }); // [ empty x 3 ]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]
a.map(..)
之所以执行失败,是因为数组中并不存在任何单元,所以 map(..)
无从遍历。join(..)
却不一样,它的具体实现可参考下面的代码:
function fakeJoin(arr,connector) {
var str = "";
for (var i = 0; i < arr.length; i++) {
if (i > 0) {
str += connector;
}
if (arr[i] !== undefined) {
str += arr[i];
}
}
return str;
}
var a = new Array(3);
fakeJoin(a, "-"); // "--"
从中可以看出,join(..)
首先假定数组不为空,然后通过 length 属性值来遍历其中的元素。而 map(..)
并不做这样的假定,因此结果也往往在预期之外,并可能导致失败。
我们可以通过下述方式来创建包含 undefined 单元(而非“空单元”)的数组:
var a = Array.apply(null, { length: 3 });
a; // [ undefined, undefined, undefined ]
虽然 Array.apply(null, { length: 3 })
在创建 undefined
值的数组时有些奇怪和繁琐,但是其结果远比 Array(3)
更准确可靠。总之,永远不要创建和使用空单元数组。
2.4.2 Date
创建日期对象必须使用 new Date()
。Date(..)
可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。
Date(..)
主要用来获得当前的 Unix 时间戳。该值可以通过日期对象中的 getTime()
来获得。从 ES5 开始引入了一个更简单的方法,即静态函数 Date.now()
。对 ES5 之前的版本我们可以使用下面的 polyfill:
if (!Date.now) {
Date.now = function(){
return (new Date()).getTime();
};
}
如果调用 Date()
时不带 new
关键字,则会得到当前日期的字符串值。
2.4.3 Error
构造函数 Error(..)
(与前面的 Array() 类似)带不带 new
关键字都可,错误对象通常与 throw
一起使用:
通常错误对象至少包含一个 message
属性,有时也不乏其他属性(必须作为只读属性访问),如 type
。除了访问 stack
属性以外,最好的办法是调用(显式调用或者通过强制类型转换隐式调用)toString()
来获得经过格式化的便于阅读的错误信息。
typeof Function.prototype; // "function"
Function.prototype(); // 空函数!
2.4.4 原生函数
Function.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
这种方法的一个好处是 .prototype
已被创建并且仅创建一次。 相反, 如果将 []
、function(){}
作为默认值,则每次调用 isThisCool(..)
时它们都会被创建一次(具体创建与否取决于 JavaScript 引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和 CPU 资源的浪费。
另外需要注意的一点是,如果默认值随后会被更改,那就不要使用 Array.prototype
。上例中的 vals 是作为只读变量来使用,更改 vals 实际上就是更改 Array.prototype
3. 数据类型检测
3.1 typeof
特点:
返回结果都是字符串,字符串中包含了对应的数据类型
"number""string"/"boolean"/"undefined"/"symbol"/"object"/"function"
局限:
typeof null => "object"
,null不是对象,它是空对象指针
检测数据或者正则等特殊的对象,返回结果都是"object",所以无法基于typeof判断是数据还是正则
console.log(typeof []); //=>"object"
console.log(typeof typeof []); //=>"string"
3.2 instanceof/constructor
特点:
检测某个实例是否属于这个类
他检测的底层机制:所有出现在其原型链上的类,检测结果都是TRUE
局限性:
由于可以基于__proto__
或者prototype
改动原型链的动向,所以基于instanceof
检测出来的结果并不一定是准确的
基本数据类型的值,连对象都不是,更没有__proto__
,虽说也是所属类的实例,在JS中也可以调取所属类原型上的方法,但是instanceof
是不认的
console.log(12 instanceof Number); //=>false
console.log(new Number(12) instanceof Number); //=>true
console.log([] instanceof Array); //=>true
console.log([] instanceof Object); //=>true
function Fn() {}
Fn.prototype.__proto__ = Array.prototype;
let f = new Fn();
//=>原型链:f -> Fn.prototype -> Array.prototype -> Object.prototype
3.3 Object.prototype.toString.call([value])/({}).toString.call([value])
特点:
不是用来转换为字符串的,而是返回当前实例所属类的信息
格式:"[object 所属类信息]"
这种方式基本上没有什么局限性,是检测数据类型最准确的方式
3.4 完整版
var class2type = {};
var toString = class2type.toString; //=>Object.prototype.toString
var hasOwn = class2type.hasOwnProperty; //=>Object.prototype.hasOwnProperty
var fnToString = hasOwn.toString; //=>Function.prototype.toString
var ObjectFunctionString = fnToString.call(Object); //=>Object.toString() =>"function Object() { [native code] }"
"Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function anonymous(item) {
class2type["[object " + item + "]"] = item.toLowerCase();
});
console.log(class2type);
function toType(obj) {
if (obj == null) {
return obj + ""; //=>return "null"/"undefined"
}
return typeof obj === "object" || typeof obj === "function" ? class2type[toString.call(obj)] || "object" : typeof obj;
}
//=>是否为函数
var isFunction = function isFunction(obj) {
return typeof obj === "function" && typeof obj.nodeType !== "number";
};
//=>检测是否为window对象
// window.window===window
var isWindow = function isWindow(obj) {
return obj != null && obj === obj.window;
};
//=>是否为纯粹的对象{}(数组和正则等都不是纯粹的对象)
var isPlainObject = function isPlainObject(obj) {
var proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
//=>getPrototypeOf获取当前对象的原型
proto = Object.getPrototypeOf(obj);
// Objects with no prototype (`Object.create( null )`)
if (!proto) {
return true;
}
// Objects with prototype are plain iff they were constructed by a global Object function
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
};
//=>是否为空对象
var isEmptyObject = function isEmptyObject(obj) {
var name;
for (name in obj) {
return false;
}
return true;
};
//=>是否为数组或者类数组
var isArrayLike = function isArrayLike(obj) {
var length = !!obj && "length" in obj && obj.length,
type = toType(obj);
if (isFunction(obj) || isWindow(obj)) {
return false;
}
return type === "array" || length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj;
};