前言
自我总结面试常问的一些细节,方便不断回顾与补充。第一次发表文章,如有问题或不足之处望及时指出。
JavaScript 原始数据类型
1.1 基础数据类型
7大基础数据类型
- boolean
- null
- undefined
- number
- string
- symbol
- BigInt (Stage阶段)
1.2 boolean
只有true、false两个值
什么是falsy?
转换成false的值称为假值(falsy value),这7个值包括undefined、null、+0、-0、NaN、false、""(空字符串)
1.3 null 和 undefined
- null值表示一个空对象指针
- undefined是一个表示”无”的原始值
比较:
- null == undefined // true
- null === undefined // false
转为数值:
- null -> 0
- undefined -> NaN
null 是对象吗?
不是
typeof null 为什么是object?
原因是在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
为什么有些地方要使用 void 0 代替 undefined
undefined在JavaScript中并不属于保留字/关键字,因此在IE5.5~8中我们可以将其当作变量那样对其赋值,于是采用void方式获取undefined则成了通用方法。
1.4 number
javascript采用IEEE754格式来表示数字,不区分整数和浮点数,javascript中的所有数字都用浮点数值表示
不让除以 0 出错,而引入了无穷大的概念
- Infinity,无穷大; => 1/+0 === Infinity
- -Infinity,负无穷大。=> 1/-0 === -Infinity
number 中经典的 0.1 + 0.2 !=0.3
原因:
- 采用 IEEE754格式的语言都有该问题
- 计算机将 0.1 0.2 表示为二进制,然后相加之后再转换成10进制
- 问题就是转换成2进制时,0.1 在二进制中是无限循环的一些数字(很多十进制小数用二进制表示都是无限循环的)
- JS 采用的浮点数标准却会裁剪掉我们的数字
- 那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002
- 0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002
- 因此结果是 0.1 + 0.2 === 0.30000000000000004
解决方案:
检查左右两边差的绝对值是否小于最小精度
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON
Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
NaN
NaN(not a number)表示非数字,NaN与任何值都不相等,包括NaN本身,且任何涉及NaN的操作都会返回NaN
判断NaN更可靠的方法是,利用NaN是javascript之中唯一不等于自身的值这个特点,进行判断
function myIsNaN(value) {
return value !== value;
}
1.5 string
字符串String类型是由引号括起来的一组由16位Unicode字符组成的字符序列
Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)
JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非 BMP(超出 U+0000 - U+FFFF 范围)的字符时,你应该格外小心。
JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。
JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
1.6 symbol
Symbol的作用非常的专一,换句话说其设计出来就只有一个目的——作为对象属性的唯一标识符,防止对象属性冲突发生。
除了自己创建的symbol,JavaScript还内建了一些在ECMAScript 5 之前没有暴露给开发者的symbol,它们代表了内部语言行为。
Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。
Symbol.toPrimitive
JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则。
An object without Symbol.toPrimitive property.
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ''); // "[object Object]"
当我们创建一个普通对象时({} 或 new Object() 的方式等),对象上是不具备 [Symbol.toPrimitive] (方法)属性的。所以,对于普通对象的到基本类型值的运算,一般按照具体场景:
- hint 值为 "string" 时,先调用 toString,toString 如果返回一个基本类型值了,则返回、终止运算;否则接着调用 valueOf 方法。
- 否则先调用 valueOf,valueOf 如果返回一个基本类型值了,则返回、终止运算;
- 否则接着调用 toString 方法。
An object with Symbol.toPrimitive property.
var obj2 = {
[Symbol.toPrimitive](hint) {
if (hint == 'number') {
return 10;
}
if (hint == 'string') {
return 'hello';
}
return true;
}
};
console.log(+obj2); // 10 -- hint is "number"
console.log(`${obj2}`); // "hello" -- hint is "string"
console.log(obj2 + ''); // "true" -- hint is "default"
如果存在Symbol.toPrimitive
则优先调用否则按照上面的规则处理。
ToPrimitive 算法在执行时,会被传递一个参数 hint,表示这是一个什么类型的运算(也可以叫运算的期望值),根据这个 hint 参数,ToPrimitive 算法来决定内部的执行逻辑。
hint 参数的取值只能是下列 3 者之一:
- string
- number
- default
1.7 BigInt
目前还在stage阶段
- BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。
- 不能使用Number和BigInt操作数的混合执行算术运算,需要通过显式转换其中的一种类型。
- 出于兼容性原因,不允许在BigInt上使用一元加号(+)运算符。
1.8 类型检查
有类型就势必会有类型检查
- typeof 对于原始数据,都可以正确检测,但是null会显示object
- typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型
- instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);
console.log(auto instanceof Car);
// expected output: true
console.log(auto instanceof Object);
// expected output: true
- so,使用instanceof判断类型就是胡扯,受限极大。
- Object.prototype.toString.call(any anyType) // "[object anyType]" 这才是最准确的判断类型的方式
Object.prototype.toString
在早期版本的 JavaScript 中,“类”的定义是一个私有属性 [[class]],语言标准为内置类型诸如 Number、String、Date 等指定了[[class]]属性,以表示它们的类。语言使用者唯一可以访问[[class]]属性的方式是 Object.prototype.toString。
var o = new Object;
var n = new Number;
var s = new String;
var b = new Boolean;
var d = new Date;
var arg = function(){ return arguments }();
var r = new RegExp;
var f = new Function;
var arr = new Array;
var e = new Error;
console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));
// ["[object Object]", "[object Number]", "[object String]", "[object Boolean]", "[object Date]", "[object Arguments]", "[object RegExp]", "[object Function]", "[object Array]", "[object Error]"]
在 ES5 开始,[[class]] 私有属性被 Symbol.toStringTag 代替,Object.prototype.toString 的意义从命名上不再跟 class 相关。我们甚至可以自定义 Object.prototype.toString 的行为,以下代码展示了使用 Symbol.toStringTag 来自定义 Object.prototype.toString 的行为:
var o = { [Symbol.toStringTag]: "MyObject" }
console.log(o + ""); // [object MyObject]
var d = new Date;
d[Symbol.toStringTag] = "custom"
Object.prototype.toString.call(d) // "[object custom]"
1.9 类型转换
JavaScript 中类型转换有三种情况
- 转换为布尔值
- 转换为数字
- 转换为字符串
转换成布尔值
方法:
- !! 两次取反
- if() 参数会自动转
- Boolean() 构造函数转
规则:
- number -> 除 0 -0 NaN 为false ,其它都为true
- string -> 除 空字符串 为false , 其它都为true
- undefined -> false
- null -> false
- object、function、array -> true
转换为数字
方法:
- Number()
- ParseInt()
- ParseFloat()
- 一元运算符 +'1' -'1'
规则:
- string -> '1' => 1 'a' => NaN
- array -> [] => 0 [1] => 1 [any] => NaN
- null -> 0
- 除数组的引用类型 -> NaN
- Symbal -> 报错
面试题:
[] == ![]
分析:
- 0 == false
- false == false
转换为字符串
方法:
- String()
- toString()
- ''+
规则:
- number -> 转换成对应的字符串 1 =>'1'
- Boolean -> 'true'
- Object -> '[object,Object]'
[题1]:+ 'b'= NaN --> 'a'+NaN = 'aNaN'
'a' + + 'b' // -> "aNaN"
1.10 装箱操作与拆箱操作
- 原始值本身是没有方法可以调用的,除非它的包装类型
- 除了 null 和 undefined,所有的原始值都有等价的、由对象包装原始值的形式表达,通过各自的构造函数可以手动进行包装,系统也会进行隐式转换
装箱操作(原始类型转对象)
console.log("abc".charAt(0)); //a
. 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。
每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。
我们定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject。
var symbolObject = (function(){ return this; }).call(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
[注意] 装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。
使用内置的 Object 函数,我们可以在 JavaScript 代码中显式调用装箱能力。
var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
拆箱操作(对象转原始类型)
在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即拆箱转换)。
- valueOf()方法返回指定对象的原始值,如果对象没有原始值,则valueOf将返回对象本身。你很少需要自己调用valueOf方法;当遇到要预期的原始值的对象时,JavaScript会自动调用它。
- toString()方法返回一个表示该对象的字符串。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError
拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。
到 String 的拆箱转换会优先调用 toString(这个不同浏览器还不一样,有的还是调用valueOf不纠结)。我们把刚才的运算从 o*2 换成 String(o),那么你会看到调用顺序就变了。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
String(o)
// toString
// valueOf
// TypeError
在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello