JS-类型&类型判断
一、类型种类
基于typeof返回值进行分类:
- number
- string
- boolean
- null
虽然
typeof null === 'object',但也作为一个类型 - undefined
- object
- function
- Symbol ES6新增类型
其中:
- 值类型(基本类型):number, string, boolean, null, undefined, Symbol
- 引用类型:object, function
值类型和引用类型区别
- 值类型变量值大小固定,不可修改(immutable),操作(比较,赋值)都是基于值的; 字符串变量虽然长度不一,但是具体的某个字符串的大小是固定的。
- 引用类型变量值大小不定,且是可变的(mutable), 操作(比较,赋值)都是基于引用的。
var a = { name: 'john'}
var b = a;
b.age = 12;
console.log(a.age) // 12
- 从存储角度看到值类型的变量值存在栈里,而引用类型的变量值存储在堆里。
二、类型转化
2.1 装箱和拆箱
基本类型(除了null, undefined)都有对应引用类型对象。并且会根据使用场景会自动地把基本类型转成对象(装箱),或者把对象转成基本类型(拆箱)。
2.1.1 装箱
'hello'.length; //
true.toString(); //
(1).toFixed(); // 为了区分数字小数点,这里添加个括号。或者这样写:1['toFixed']()
显示装箱
除了调用基本类型对应的引用类型构造函数外,可以利用Object方法进行装箱。
Object(1) // 等价 new Number(1)
Object(false) // 等价new Boolean(false)
Object('hello') // 等价new String('hello')
// null, undefined没有对应的引用类型
Object(null) // 空对象`{}`
Object(undefined) // 空对象`{}`
PS:可以利用Object(null)判断参数是否为非null的对象。
function isObj(target) {
return Object(target) === target;
}
2.1.2 拆箱
当需要基本类型时,对象也会自动转成基本类型(拆箱)
1. 转number(比如算术运算符中的对象,需要转成number)
- 如果定义了
valueOf方法并且该方法返回原始值,则调用该方法,并转成number; 内置对象中只有Boolean,String,Number,Date重写了valueOf方法,其他大部分都还是默认的valueOf行为。 - 否则,如果定义了
toString方法并且该方法返回原始值,则调用该方法,并转成number; - 其他情况报类型错误
TypeError: Cannot convert object to primitive value
2. 转string比如+运算中的对象(比如模板字符串中)
- 如果定义了
toString方法并且该方法返回原始值,则调用该方法,并转成string; - 否则,如果定义了
valueOf方法并且该方法返回原始值,则调用该方法,并转成string; - 其他情况报类型错误
TypeError: Cannot convert object to primitive value
注意:转number和转string的过程正好相反。
3. 转boolean(比如逻辑运算中)
- 所有对象(不包含null)都是
true。 即使是new Boolean(false)对象 - **注意: **
new Boolean(false)和Boolean(false)返回值是不一样的,前者是对象,后者是基本值。
4. Demo
var obj = {
toString: function() {
return 1
},
valueOf: function() {
return 2
}
}
console.log(`${obj}`) // '1', 调用`toString`
console.log(obj + 1) // 3, 调用`valueOf`
console.log(obj + '1') // '21', 调用`valueOf`, 然后再转成字符串‘2’
// Demo2
var obj2 = {
toString: function() {
return {} // 返回对象
},
valueOf: function() {
return 2
}
}
console.log(`${obj2 }`) // '2', 调用`valueOf`(因为toString返回值不是原始值)
// Demo3
var obj3 = {
toString: function() {
return 1
},
valueOf: function() {
return {}
}
}
console.log(`${obj3 }`) // '1', 调用`toString`
console.log(obj3 + 1) // 2, 调用`toString`, 因为valueOf返回值不是原始值
console.log(obj3 + '1') // '11', 调用`toString`, 然后再转成字符串‘1’
2.2 参与运算的类型转换
运算符对表达式的类型有要求,不满足要求的会进行隐式的类型转换。但有些运算符可能支持多种类型的数据,这里就涉及一些优先类型转换规则:
- 算术运算符
+(string和number都可以) 当其中一个是字符串时,会把另一个原始值(拆箱之后)转成字符串。 - 比较运算符(
>,>=,<=,<)(string和number都可以) 都是string时才进行字符串比较,其他转number进行比较 - 相等运算符(
==,!=)(基本类型和引用类型都可以)
- 相同类型的直接判断值(同
===); -
null和undefined值不会发生类型转换,即效果同===,但是特例null == undefined为true;
null == false; // false
null == 0; // false
null == ''// false
undefined == false; // false
undefined == 0; // false
undefined == '' // false
undefined == null // true
null和undefined没有封装类型,也没有方法(不存在valueOf, toString方法)
- 对象转成基本类型(number场景的转换规则)参与
==比较; - 基本类型(除了
null,undefiend)都转成Number进行比较,即String和bool不会互转,都会统一转成Number。
false == ''; // true
false == 0; // true
'' == 0; // true
2.2.2 特例
- 空字符串转number是0,即
+'' === 0; 注意+[] === 0,因为空数组的toString返回是空字符串。
2.2.3 总结下:
- 运算中的拆包都走转Number拆箱规则;
- 只有
+中字符串优先级高些,其他基本上都是转number; - 分析类型转换时,可能不能一步明确目标类型,可能会涉及多次类型转换,比如上例中的对象和字符串加操作。
2.3 显示类型转换
2.3.1 使用构造函数Boolean, Number, String, Object可以显示的转成指定的引用类型。
new Number('1') // => Number {1}
new Boolean([]) // => Boolean {true}
new Object(3) // => Number{3}
- 有原始值的对象,如
Number/Boolean/String的函数调用和new方式调用返回值不一样;
Number('1') // => 原始值 1
new Number('1') // 对象 Number{1}
- 没有原始值的内置对象,如Array, Object, RegExp的函数调用等价new方式调用。
- Object函数调用这么智能啊,可以根据实参类型转成对应的对象格式。
null和undefined不存在对应的对象,Object(null)和Object(undefined)返回空对象{}。
2.3.2 一元运算符显示转化
+'1' // => 1
!!1 // => true
'' + 1 // => '1'
- 这里是借助有些运算符只支持某一种类型数据的原理;
- 逻辑非
!运算符不涉及拆包过程(所有对象都是true)。
2.3.3 内置方法
-
parseInt(string[, radix])
- radix只是表示参数1的进制,返回值是10机制,不是radix指定返回值的进制
实参
null,undefined,0是无效值,就当做没有传值。 非int值会转成Int。 -
parseInt是逐个字符解析实参string的,遇到非法字符或者字符串结尾,就把已经解析的结果返回,如果第一个字符无法解析则返回NaN。
parseInt('a1') // NaN
parseInt('1a') // 1,这个跟`+'1a'`结果不一样
parseInt('123', 5) // 将'123'看作5进制数,返回十进制数38 => 1*5^2 + 2*5^1 + 3*5^0 = 38
parseInt("546", 2); // 除了“0、1”外,其它数字都不是有效二进制数字
-
parseFloat(string)
三、类型判断
1.typeof虽然简单方便,但不能区分null和object以及各种常用内置对象;
typeof null // "object"
typeof /abc/ // "object"
typeof [] // "object"
typeof (function(){}) // "function"

但是如果只是识别函数,也可以使用typeof (常见于第三方库/util中)。
原理:???
In the first implementation of JavaScript, JavaScript values were represented as a type tag and a value
2. 更常规的用法调用Object.prototype.toString方法。
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call('')); // [object String]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call( {})); // [object Object]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
var ObjProto = Object.prototype;
var toString = ObjProto.toString;
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError, isMap, isWeakMap, isSet, isWeakSet.
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
原理:
- 内置对象都有默认的
toString格式字符串(转字符串标签)。
toString() returns "[object type]", where type is the object type.
这里的type应该是指对象默认描述字符串。
-
toString方式更多的是判断各种对象类型,所以对于基本类型会有装箱的操作(除了null, undefined)。从ES5开始null, undefined也可以调用了
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
缺点:
- 不可靠,可以利用
Symbol.toStringTag修改默认的toString字符串;
var myObj = {
get [Symbol.toStringTag]() {
return 'MyObj'
}
}
console.log(myObj[Symbol.toStringTag]) // "MyObj"
console.log(Object.prototype.toString.call(myObj)); "[object MyObj]"
- 无法区分
NaN,Infinity
3. 区分NaN, Infinity值
NaN, Infinity都是number值(它俩类型一样),使用方法2是不能区分的。
Object.prototype.toString.call(Infinity) // "[object Number]"
Object.prototype.toString.call(NaN) // "[object Number]"
还好Number有相关静态方法isNaN, isInfinity(宿主也有同名的全局方法)。
4. Array.isArray
Array.isArray() exists because of one particular problem in browsers: each frame has its own global environment
instanceof都存在这个问题,为啥单独给只提供个Array.isArray?
其他方式或多或少有点问题,Array.isArray更靠谱些。
四、参考
String
一、String
1.1 字符串length & 字符数量
注意,注意,注意:字符的length属性表示的是字符串长度是以UTF-16码点为单位的(两个字节),并不是字符的数量。
console.log('A你Z'.length) // 4
原因:
在JS出生的时候,Unicode编码规范最大只有两个字节(BMP字符),所以那个时候length也就是字符的数量。
如何获取字符串的数量?
ES6方法
- 利用字符串是可迭代对象
// 展开操作符
console.log([...'A你Z'].length) // 3
// for-of
var count = 0;
for(let c of 'A你Z') {
count++
}
console.log(count) // 3
var str = 'A你Z'
var strArr = [];
var maxCharCode = Math.pow(2, 16) -1;
for(var i = 0, len = str.length; i < len; ++i) {
var charCode = str.codePointAt(i);
if(charCode <= maxCharCode ) {
strArr.push(str[i])
} else {
strArr.push(str[i] + str[++i]) // 非BMP字符
}
}
console.log(strArr.length) // 3
ES3/5方法
只能用charCodeAt hack ES6的方式了。
字符串有最大长度吗?
String.length有关于字符串长度的描述:
ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified. In Firefox, strings have a maximum length of 230 - 2 (~1GB). In versions prior to Firefox 65, the maximum length was 228 - 1 (~256MB).
在之前呢?如果字符串应该以数组的形式存储的,那应该受最大索引的限制(Math.(2. 32) - 1),如果这样的话大概最大
将近4G((Math.pow(2, 32) -1) / 1024 / 1024 / 1024)。字符串要不存储的,估计计算机内存直接被耗尽了。
试了下,结果浏览器直接报内存不足。。。
Array.from({ length: Math.pow(2, 32) - 1}).fill().join('0')
1.2 字符串索引
跟length属性一样,字符串的索引也是以UTF-16为单位的。
console.log('A你Z'[1].charCodeAt(0).toString(16)) // d87e
1.3 字符串和数组
字符可以视为字符构成的数组,很多操作跟数组类似:
- 都可以通过索引访问元素;
- 存在类似的方法;
- 并且字符串
String也是可迭代对象。
var arr = [...'hello']; // ["h", "e", "l", "l", "o"]
字符串转数组
var str = 'A你Z'
console.log(Array.from('A你Z')) // ["A", "你", "Z"]
console.log([...'A你Z']) // ["A", "你", "Z"]
console.log('A你Z'.split('')) // ["A", "�", "�", "Z"]
console.log(Array.prototype.slice.call('A你Z')) // ["A", "�", "�", "Z"]
// codePointAt
var strArr = [];
for(var i = 0, len = str.length; i < len; ++i) {
var charCode = str.codePointAt(i);
if(charCode < Math.pow(2, 16)) {
strArr.push(str[i])
} else {
strArr.push(str[i] + str[++i]) // 非BMP字符
}
}
console.log(strArr) // ["A", "你", "Z"]
-
ES3/5的方法(比如
split('')和Array.prototype.slice.call)是基于索引的,针对非BMP的字符是无法正常转数组的。 -
ES6基于迭代器的方法是基于Unicode码点的。
1.4 字符大小
字符可以直接进行逻辑运算的。利用逐个比较每个字符的UTF-16码点值进行比较的。Array.prototype.sort方法也是利用这个方式比较字符串的。
The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.
注意比较是UTF-16码点值不是Unicode码点值。
1.5 字符加法 & 字符拼接
- 可以直接使用
+进行字符串拼接。 -
concat方法 - 利用
Array.prototype.join方法。
1.6 字符串字面量
- 正常格式
"abc",单引号和双引号都行; - Unicode格式
\uHHHH(十六进制)。“\u007A” === "z"两个UTF-16字节,如果展示非BMP字符得用两个\uHHHH(代理对)。
console.log('\uD83D\uDE80')
- ES6新引入的Unicode格式
"\u{...}"(1个或多个十六进制Unicode码点值)。'\u{7A}' === 'z'展示非BMP字符更方便了。
console.log('\uD83D\uDE80')
console.log('\u{1F680}');
二、字符存储 & UTF-16
2.1 两个字节存储一个字符
字符串的每个字符都是采用UTF-16进行编码存储的。即采用两个字节存储一个字符,理论上最多可表示2^16 (65,536)个字符,实际上表示字符的没使用这么多,因为有些区域的码点值有特殊的用途。
UTF-16码点值:每个字符对应的编码值,即charCodeAt方法的返回值。
BMP(Basic Multilingual Plane)区域:在Unicode字符码表里两个字节表示区域叫做BMP区域。
在BMP区域UTF-16码点值和Unicode码点值是一样的。
2.2 非BMP区域字符存储
采用两个UTF-16码点值表示(即4个字节)一个字符。这两个码点值叫代理对:
- 左侧的叫高位代理对,取值范围
[0xD800, 0xDBFF] - 右侧的叫低位代理对,取值范围
[0xDC00, 0xDFFF]
高位代理的码点值为啥小于低位代理码点值

- 高位代理的码点值小于低位代理码点值。
- 低位代理后面还有BMP字符。
这导致非BMP字符不能正确的进行逻辑大小比较和排序。
代理对和unicode码点值转换
有专门的转化公式。在利用charCodeAt hackcodePointAt时也得利用这个公式。
三、APIs
3.1 replace/replaceAll
本质copy是字符串,并同时进行替换操作。是字符串格式化的利器,功能也很强大。
-
replace(regexp|substr, newSubstr|function)copy的同时,对字符进行一次或者多次(取决于正则是否带g)匹配替换。 -
replaceAllcopy的同时,对字符进行多次(此时如果是正则,则必须带g)匹配替换。
replaceAll和replace差异主要体现在非正则的实参上,正则实参本质没差异(你品,你细品)。
⚠️⚠️⚠️实际项目里最好不要使用replaceAll,这个方法的兼容性太差:

参数1: 匹配的字符串或则正则表达式
参数2:被替换的字符串或则字符串生成函数
-
字符串 除了是普通字符串,还支持字符串占位符
-
字符串生成函数
function(str, $1, $2...., offset, input, groups)参数和String.match单次匹配返回值,RegExp.exec返回值都比较类似。 但是注意的是分组后面的实参offfset是表示字符串的下标,不是正则的lastIndex。并且在匹配领宽度字符时存在差异了:
var regExp = /(?=1)/g;
var match = regExp.exec('312121212')
console.log(regExp.lastIndex, match.index) // 1 1
match = regExp.exec('312121212')
console.log(regExp.lastIndex, match.index) // 1 1
var offsetArr = []
'312121212'.replace(/(?=1)/g, function(str, offset) {
offsetArr.push(offset);
})
console.log(offsetArr) // [1, 3, 5, 7]
exec在匹配零宽度正则时感觉就是bug的存在,此时如果lastIndex==0还好理解(即领宽度不占用匹配字符宽度)。
3.2 match / matchAll
-
matchAll(regexp)比较简单,就是返回所以匹配的字符集合
- 返回值不是数组,是个可迭代对象,没有匹配成功,则是个空可迭代对象;
- 返回值可迭代对象每个元素,跟
RegExp.prototype.exec返回值一样。
-
match(regexp)对字符进行一次或者多次(取决于实参提供的正则是否具g)匹配
-
如果是多次匹配,则返回值类似
matchAll(regexp)区别是match(regexp)返回的是个数组; -
如果是单次匹配,则返回值同
RegExp.exec。
3.3 split
3.4 迷人三剑客:charAt/charCodeAt/codePointAt
-
charAt(index)获取指定索引位置的字符 -
charCodeAt获取指定索引位置的UTF-16码点值(范围[0, 65535 ]) 跟charAt(index)是类似的,不过返回值不同。一个是字符,一个是字符对应的码点值。 -
codePointAt获取指定索引位置的Unicode码点值
- ES2015引入的
- 如果指定的索引位置字符是BMP字符或则不是UTF-16代理对的起点,则返回值同
charCodeAt
var str = 'A你Z'
console.log(str.charCodeAt(1)) // 55422
console.log(str.codePointAt(1)) // 194564
console.log(str.charCodeAt(2)) // 56324
console.log(str.codePointAt(2)) // 56324
charAt(index)& []下标获取区别
两者在功能上是等价的。
var str = 'A你Z'
console.log(str[1] === str.charAt(1))
chartAt是标准规范的访问方式,String又是伪数组,ES5新增了[]访问方式。一句话后者只是前者的语法糖,并且是在ES5中才实现的。
如果非要说些两者的区别,那就得是参数类型:
-
charAt的参数是整型Number,其范围是[0, length-1], 不在该范围内的,则返回空串;
- 如果参数不是整型,则会先转成整型Number;
- 如果参数转换结果为NaN,则视为0(相当于执行
NaN >>> 0)。
-
[index]下标方式中index是个字符串(跟一般对象通过属性访问方式一样)
- 如果不是字符串则转成字符串,
- 如果访问不到,则返回
undefined。
'abc'.charAt(true) // 'b'
'abc'[true] // undefined
'abc'.charAt(1.2) // 'b'
'abc'[1.2] // undefined
如何获取指定位置的完整字符
这个问题本身就存在问题。一般我们指定的“位置”是索引,但是把索引映射到字符的位置不是很明显。 如果是明确“获取第几个字符”,则可以利用迭代器遍历或者先把字符串转成字符数组再提取。
参考
Number
一、语法
1.1 概述
- JS数字不区分整形和浮点数,统一采用IEEE 754标准64位方式存储。
- JS虽然没有整形但是有些操作是基于32整数进行的
- 位运算
- 数组索引最大长度
- setTimeout/setInterval的最大delay参数值
1.2 NaN
NaN是个全局对象属性,表示一个特殊的数字Not a Number。
- 类型是
Number,但不具有数字的运算特性,甚至相等性判断中跟自己也不相等。
console.log(NaN === NaN) // false
console.log(NaN !== NaN) // true
- 常见产生
NaN的场景:
-
0/0,Infinity / Infinity,Infinity * 0; - 有
NaN参与的数学运算; - 非数字的字符串,undefined转Number操作(如
+'hello',+undefined)
- 不只是JS才有
NaN,它来自标准 IEEE-754 - 计算机中的浮点数其实是有限的,对于那些无法存储的浮点数统一用
NaN表示; -
NaN更多的是表示一个逻辑结果,而导致NaN的逻辑由很多,所以 这也是NaN不等于自身的原因吧。
1.3 Infinity
Infinity是个全局对象属性,表示一个特殊的数字:无穷大。
console.log(Infinity); /* Infinity */
console.log(Infinity === Infinity) // true
console.log(Infinity + 1 ); /* Infinity */
console.log(Math.pow(10, 1000)); /* Infinity */
console.log(Math.log(0) ); /* -Infinity */
console.log(1 / Infinity ); /* 0 */
console.log(1 / 0 ); /* Infinity */
1.3.1 利用Infinity识别-0和0
ES6中引入同值相等算法是可以区分-0和0的,在此之前可以利用Infinity识别:
// JS中0可以作为被除数
console.log(1/0 === Infinity) // true
console.log(1/-0 === -Infinity) // true
1.4 字面量
Number字面量有多重写法。
- ES3/5支持十进制,十进制科学计数法,16进制(hex)字面量;
- ES2015支持二进制(binary ),八进制( octal)直面量。
var a = 11; // 十进制
var b = 2E2; // 十进制科学计数法,E大小写不区分
var c = 0xf; // 16进制,x大小写不区分
var d = 0b110; // 二进制,b大小写不区分
var e = 0o10; // 8进制,o大小写不区分
var f = 010; // 8进制,此时`o`可以省略
1.5 APIs
ES2015
1. Number.MAX_SAFE_INTEGER
Math.pow(2, 53) - 1
53 = 利用52位mantissa区域+1位省略。整数是需要连续的,所以表示整数时不能使用指数位区域。
const x = Number.MAX_SAFE_INTEGER + 1;
const y = Number.MAX_SAFE_INTEGER + 2;
console.log(Number.MAX_SAFE_INTEGER);
// expected output: 9007199254740991
console.log(x +3);
// expected output: 9007199254740992
console.log(x === y); // true
2. isNaN函数和Number.isNaN方法区别
都是用于判断一个值是否为NaN。但是Number.isNaN更严谨。
-
Number.isNaN不会进行参数转换,而isNaN会默认把实参先转成Number(用于类型判断的方法居然进行了类型转换,果断不能用)
console.log(Number.isNaN('a')); // false
console.log(isNaN('a')); // true
-
Number.isNaN是ES2015引入的,兼容性差些,但是也要使用,不支持就用polyfill:
Number.isNaN = Number.isNaN || function isNaN(input) {
return typeof input === 'number' && input !== input;
}
2. isFinite函数和Number.isFinite方法区别
跟isNaN函数和Number.isNaN方法区别类似。
-
isFinite会对实参进行类型转换,而Number.isFinite则不会。 -
Number.isFinite是ES2015引入的
if (Number.isFinite === undefined) Number.isFinite = function(value) {
return typeof value === 'number' && isFinite(value);
}
3. Number.isInteger
-
XXX.0也是作为整数;
Number.isInteger(5.0); // true
- polyfill
Number.isInteger = Number.isInteger || function(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value;
};
二、存储:了解IEEE双精度浮点数
2.1、复习10进制转2进制
- 整数部分:除2取余,逆序
- 小数部分:乘2取整,正序 在线工具
2.2、了解IEEE 754双精度浮点数规范
2.2.1 通过2进制的科学计数法表示
和10进制的科学计数法类似,二进制的科学技术法格式为1.xxx*2^N。其中需要留意下二进制科学计数法的整数部分都是1,所以在存储时省略整数部分1。

2.2.2 格式:符号位+指数位+尾数位
- 符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
- 指数位E:中间的 11 位存储指数(exponent),用来表示次方数 科学计数法中指数E是可以为负数的,在表示负的指数时IEEE754标准引入了一个偏移量1023,在存储指数时加上该偏移量把负数E转成正数。这就导致11位的指数能够表示指数的范围是[-1023, 1024]。
- 尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零,没有填满的部分自动补0
如10进制数400.12,用10进制科学计数法表示为:4.0012*10^2,。其中"0012"就是尾数部分。
最终可表示为(图片来源):
其中S,E,M都是实际存储科学计数法的值。
如10进制4.5转成2进制为:
// Step1 转成二进制
100.1
// Step2 转成二进制科学计数
1.001*2^2
S = 0
E = 2 + 1023 = 2015
M = 001 // 整数1被省略了
形象的查看存储情况,可参考这里
2.2.3 有限集合
IEEE754能表示的实数数量是有限的,并且浮点数也不是连续的。
假设MAX_VALUE, MIN_VALUE分别表示其表示的最大正数和最小正数,那有限集合可表示为(并且数字不是连续的):
[-MAX_VALUE, -MIN_VALUE] U [MIN_VALUE, -MAX_VALUE]
2.3 几个有趣的问题
1. 0.1 + 0.2 !== 0.3
0.1,0.2和0.3在转成二进制时都是无限循环的,在存储时会失去精度,0.1+0.2在运算时也会失去精度,导致结果不等于0.3。
2. 给一个数字加上一个非0的增量还等于本身这个数字
1 + Number.EPSILON/2 === 1
这个增量如果小于JS能表示的最小浮点数就会视为0,加上这样的数等于加上0。
3. JS最大整数为啥是2^53-1而不是2^52-1
整数需要连续性,所以表示整数时不能使用指数位E区域,只有尾数M区域可表示连续的数据
尾数占用52个bit,再加上省略的那个bit(见2.1)正好53个bit。
4. JS可以精确的表示哪些小数呢
小数部分是这种格式的都可以精确表示。
1/Math.pow(2, N), 其中N是(0, 1024)区间的整数。
如分数1/2,1/4, 1/8。
三、大数处理
大数处理已经有成熟的库了decimal js了,并且ES2015也引入了新的数据类型BigInt。如果让自己实现该怎么做呢?
整体思路:就是用字符串存储大数。
3.1 加法
- 低位对齐,逐个累加,进位;
- 符号位:一正一负转减法。
function getSign(a) {
return /^-/.test(a) ? 1 : 0;
}
function add(a, b) {
a = a + '';
b = b + '';
var aSign = getSign(a);
var bSign = getSign(b);
// 有一个负数,转成减法
if(aSign^bSign) {
return aSign ? minus(b, a) : minus(a, b);
}
// 负数相加
if(aSign) {
a = a.substring(1);
b = b.substring(1);
}
var maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength , 0);
b = b.padStart(maxLength , 0);
var result = [];
var overflow = 0;
for(var i = maxLength - 1; i >= 0; i--) {
var sum = +a[i] + (+b[i]) + overflow;
overflow = Math.floor(sum / 10);
if(overflow) {
sum %= 10;
}
result[i] = sum;
}
// 最后一位
if(overflow) {
result.unshift(overflow);
}
// 存在符号
if(aSign) {
result.unshift('-');
}
return result.join('');
}
console.time(1)
console.log(add('9007199254740991', '1234567899999999999'))
console.timeEnd(1)
跟decimal js对比了下性能差不多。
3.2 减法
跟加法类似,但是有几点比较特殊。
- 低位对齐,逐位减,不够则借1。
- 符号位:
- 一正一负转加法;
- 都是负数转成正数减正数
- 小数减去大数,要转成大数减小数+负号。
function minus(a, b) {
a = a + '';
b = b + '';
var aSign = getSign(a);
var bSign = getSign(b);
// a-(-b), -a-b转成加法
if(aSign^bSign) {
return add(aSign, bSign ? b.substring(1) : '-' + b);
}
// 都是负号,转成b-a
if(aSign) {
return minus(b, a);
}
// 剩下的就是正整数相减了
var maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, 0);
b = b.padStart(maxLength, 0);
result = [];
var overflow = 0, left;
for(let i = maxLength - 1; i >= 0; --i) {
left = +a[i] - (+b[i]) - overflow;
overflow = left < 0 ? 1 : 0;
if(overflow) {
left += 10;
}
result.unshift(left);
}
// 最高位也发生借1,则转成大数减小数
if(overflow) {
return '-' + minus(b, a);
}
return result.join('')
}
3.3 相乘
相乘就是累加啊。有几点比较特殊。
- 出现0/NaN/Infinity,, 则返回0/NaN/Infinity;
- 竖式相乘+累加。
function multi(a, b) {
a = a + '';
b = b + '';
var aSign = getSign(a);
var bSign = getSign(b);
if(aSign) {
a = a.substring(1);
}
if(bSign) {
b = b.substring(1);
}
var sumTotal = 0
for(let i = a.length - 1; i >= 0; --i) {
// 低位补0
var sum = Array(a.length - 1 - i).fill(0);
var overflow = 0;
for(let j = b.length - 1; j >= 0; --j) {
var c = +a[i] * (+b[j]) + overflow;
overflow = Math.floor(c/10);
if(overflow) {
c = c % 10;
}
sum.unshift(c);
}
// 高位也溢出了
if(overflow) {
sum.unshift(overflow);
}
// 累加
sumTotal = add(sumTotal, sum.join(''));
}
// 处理负号为
if(aSign^bSign) {
sumTotal = '-' + sumTotal;
}
return sumTotal
}
测试发现跟decimal js性能差很多,因为进行了多次大数相加处理。 有啥优化方式?Google了下原来有相关个算法leetcode 43.字符串相乘
var mul = function(num1, num2) {
if(isNaN(num1) || isNaN(num2)) return '' //判断输入是不是数字
var len1 = num1.length,
len2 = num2.length
var ans = []
for (var i = len1 - 1; i >= 0; i--) { //这里倒过来遍历很妙,不需要处理进位了
for (var j = len2 - 1; j >= 0; j--) {
var index1 = i + j
var index2 = i + j + 1
var mul = num1[i] * num2[j] + (ans[index2] || 0)
ans[index1] = Math.floor(mul / 10) + (ans[index1] || 0)
ans[index2] = mul % 10
}
}
var result = ans.join('')
return +result === 0 ? '0' : result.replace(/^0+/,'')
}
算法分析优化版竖式
参考
- IEEE 754 计算器
- IEEE 754 二进制表示
- MDN Number
- JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后 顺便可以复习下数字存储原码,反码,补码。
undefined
一、概述
ECMAScript中Undefined(首字母大写)类型定义是:有且只有一个undefined(首字母小写)值的类型。任何没有赋值的变量的值都是undefined。
var a;
console.log(a); // undefined
但Undefined类型只存在于规范中,实际实现中并没有定义Undefined类型。 在浏览器上下文中undefined是全局变量window的成员变量(一般是只读的),既然undefined是全局属性变量,那他肯定不是保留字了。所以我们也可以定义名为undefined属性了,并且老的JS中可以重写window.undefiend属性值:
var a;
console.log(a === window.undefined); // true
;(function(){
var undefined = 'hello';
console.log(undefined); // hello
console.log(a === window.undefined); // true
console.log(a === undefined); // false
})();
二、void 0替代undefined
void运算符返回的是undefined。
var a;
;(function(){
var undefined = 'hello';
console.log(a === void 0); // true
console.log(a === undefined); // false
})();
代码中常常看到使用void 0替代undefined(打包工具也会自动转换),这是为啥呢?: underscorejs isUndefined
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
2.1 void 0 更安全
如上文,undefined值可以作为变量的,万一值被重新了,那岂不是凌乱了。
2.2 void 0 体积小
好多代码压缩工具都会把undefined替换成void 0。字符串“void 0” 比“undefined”更短一些(额,少了3个字符)。
参考
练习
1. 输出以下代码的执行结果并解释为什么
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
typeof 内部
根据变量的类型标签(type tag)获取类型信息的。JS是动态类型的变量。每个变量在存储时除了存储变量值还需要存储变量的类型。JS里使用32位(bit)存储变量信息。低位的1~3个bit存储变量类型信息: mozilla
#define JSVAL_OBJECT 0x0 /* untagged reference to object */
#define JSVAL_INT 0x1 /* tagged 31-bit integer value */
#define JSVAL_DOUBLE 0x2 /* tagged reference to double */
#define JSVAL_STRING 0x4 /* tagged reference to string */
#define JSVAL_BOOLEAN 0x6 /* tagged boolean value */
.... XXXX X000 // object
.... XXXX XXX1 // int
.... XXXX X010 // double
.... XXXX X100 // string
.... XXXX X110 // boolean
- 只有
int类型的type tag使用1个bit,并且取值为1,其他都是3个bit, 并且低位为0。这样可以通过type tag低位取值判断是否为int数据。 - 相当于使用2个bit区分这四个类型:
object,double,string,boolean。
如何标记null, undefined和function的呢?
-
undefined特殊处理,赋值特殊的值(-2^30); -
null采用机器码NULL指针值(取值也是0x00),这也导致了一个无法修复的bug
typeof null // "object"
-
function函数也是对象(可调用对象),所以函数的type tag也是000。但是函数作为JS的一等公民,也被视为一种变量类型。typeof逻辑内部也特殊处理了:如果对象具有([[call]]内部方法)[],则返回function。
typeof执行逻辑
- 先获取变量的类型信息;
- 根据类型信息返回对应的类型字符串信息,如图:
获取变量类型的逻辑直接贴代码吧:
JS_TypeOfValue(JSContext *cx, jsval v)
{
// #define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
JSType type = JSTYPE_VOID;
JSObject *obj;
JSObjectOps *ops;
JSClass *clasp;
CHECK_REQUEST(cx);
// #define JSVAL_IS_VOID(v) ((v) == JSVAL_VOID)
if (JSVAL_IS_VOID(v)) {
type = JSTYPE_VOID;
} else if (JSVAL_IS_OBJECT(v)) {
obj = JSVAL_TO_OBJECT(v);
if (obj &&
(ops = obj->map->ops,
ops == &js_ObjectOps
? (clasp = OBJ_GET_CLASS(cx, obj),
clasp->call || clasp == &js_FunctionClass)
: ops->call != 0)) { // 具有call属性
type = JSTYPE_FUNCTION;
} else {
type = JSTYPE_OBJECT;
}
} else if (JSVAL_IS_NUMBER(v)) { // int 和double都是number类型
type = JSTYPE_NUMBER;
} else if (JSVAL_IS_STRING(v)) {
type = JSTYPE_STRING;
} else if (JSVAL_IS_BOOLEAN(v)) {
type = JSTYPE_BOOLEAN;
}
return type;
}
参考
Object.prototype.toString()内部
1. 内部属性[[Class]]
用于标记对象具体类型的描述字符串,用于区分各种对象。一般内置对象会有这个值:
"Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", and "String".
2. Object.prototype.toString()语法
返回对象的默认的字符串表示。
思考
什么样的字符串能代表一个对象呢?
类型,对象的子类型。估计这也是返回值的格式是[object subType]的原因。
-
object表示是个对象类型。 -
subType区分各种类型的内置对象。
3. Object.prototype.toString()逻辑
- If the this value is undefined, return "[object Undefined]".
- If the this value is null, return "[object Null]".
- Let O be ! ToObject(this value).
- Let isArray be ? IsArray(O).
- If isArray is true, let builtinTag be "Array".
- Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments".
- Else if O has a [[Call]] internal method, let builtinTag be "Function".
- Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
- Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
- Else if O has a [[NumberData]] internal slot, let builtinTag be "Number".
- Else if O has a [[StringData]] internal slot, let builtinTag be "String".
- Else if O has a [[DateValue]] internal slot, let builtinTag be "Date".
- Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
- Else, let builtinTag be "Object".
- Let tag be ? Get(O, @@toStringTag).
- If Type(tag) is not String, set tag to builtinTag.
- Return the string-concatenation of "[object ", tag, and "]".
综上:
- 特殊处理
null,undefined它俩没有对应的构造函数,无法装箱成对象 - 基本类型数据,转成对象
- 获取对象的
[[Class]]内部属性值,和Symbol.toStringTag属性值"toStringTag" - 如果"toStringTag"是字符串,则返回
[object, toStringTag],否则返回[object, [[Class]]
Object.defineProperty(String.prototype, Symbol.toStringTag, {
get() {
return 'MyString'
}
})
var a = "test"
Object.prototype.toString.call(a); // "[object MyString]"
参考
深入浅出JS类型判断
JS中判断数据类型的方式有很多
- typeof
- Object.prototype.toString
- instanceof
- Array.isArray
一、回顾
JS数据类型分为基本类型和引用类型。 基本类型:
- undefined
- null
- Number
- String
- Boolean
- Symbol
引用类型
- Object
- Function
函数是一种特殊的对象,即可调用的对象。
二、typeof
2.1 语法
typeof操作符可以区分基本类型,函数和对象。
console.log(typeof null) // object
console.log(typeof undefined) // undefined
console.log(typeof 1) // number
console.log(typeof 1.2) // number
console.log(typeof "hello") // string
console.log(typeof true) // boolean
console.log(typeof Symbol()) // symbol
console.log(typeof (() => {})) // function
console.log(typeof {}) // object
console.log(typeof []) // object
console.log(typeof /abc/) // object
console.log(typeof new Date()) // object
-
typeof有个明显的bug就是typeof null为object; -
typeof无法区分各种内置的对象,如Array,Date等。
2.2 原理
JS是动态类型的变量,每个变量在存储时除了存储变量值外,还需要存储变量的类型。JS里使用32位(bit)存储变量信息。低位的1~3个bit存储变量类型信息,叫做类型标签(type tag)
.... XXXX X000 // object
.... XXXX XXX1 // int~~~~
.... XXXX X010 // double
.... XXXX X100 // string
.... XXXX X110 // boolean
- 只有
int类型的type tag使用1个bit,并且取值为1,其他都是3个bit, 并且低位为0。这样可以通过type tag低位取值判断是否为int数据; - 为了区分
int,还剩下2个bit,相当于使用2个bit区分这四个类型:object,double,string,boolean; - 但是
null,undefined和Function并没有分配type tag。
如何识别Function
函数并没有单独的type tag,因为函数也是对象。typeof内部判断如果一个对象实现了[[call]]内部方法则认为是函数。
如何识别undefined
undefined变量存储的是个特殊值JSVAL_VOID(0-2^30),typeof内部判断如果一个变量存储的是这个特殊值,则认为是undefined。
#define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
如何识别null
null变量存储的也是个特殊值JSVAL_NULL,并且恰巧取值是空指针机器码(0),正好低位bit的值跟对象的type tag是一样的,这也导致著名的bug:
typeof null // object
很不幸,这个bug也不修复了,因为第一版JS就存在这个bug了。祖传代码,不敢修改啊。
有很多方法可以判断一个变量是一个非null的对象,之前遇到一个比较经典的写法:
// 利用Object函数的装箱功能
function isObject(obj) {
return Object(obj) === obj;
}
isObject({}) // true
isObject(null) // false
三、Object.prototype.toString
一般使用Object.prototype.toString区分各种内置对象。
3.2 语法
console.log(Object.prototype.toString.call(1)); // [object Number],隐式类型转换
console.log(Object.prototype.toString.call('')); // [object String],隐式类型转换
console.log(Object.prototype.toString.call(null)); // [object Null],特殊处理
console.log(Object.prototype.toString.call(undefined)); // [object Undefined],特殊处理
console.log(Object.prototype.toString.call(true)); // [object Boolean],隐式类型转换
console.log(Object.prototype.toString.call( {})); // [object Object]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
- 如果实参是个基本类型,会自动转成对应的引用类型;
Object.prototype.toString不能区分基本类型的,只是用于区分各种对象; -
null和undefined不存在对应的引用类型,内部特殊处理了;
3.3 原理
内部属性[[Class]]
每个对象都有个内部属性[[Class]],内置对象的[[Class]]的值都是不同的("Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"),并且目前[[Class]]属性值只能通过Object.prototype.toString访问。
Symbol.toStringTag属性
其实Object.prototype.toString内部先访问对象的Symbol.toStringTag属性值拼接返回值的。
var a = "hello"
console.log(Object.prototype.toString.call(a)); // "[object String]"
// 修改Symbol.toStringTag值
Object.defineProperty(String.prototype, Symbol.toStringTag, {
get() {
return 'MyString'
}
})
console.log(Object.prototype.toString.call(a)); // "[object MyString]"
如果哪个货偷偷修改了内置对象的Symbol.toStringTag属性值,那Object.prototype.toString也就不能正常工作了。
3.4 Object.prototype.toString内部逻辑
综上可以总结Object.prototype.toString的内部逻辑:
- 如果实参是
undefined, 则返回"[object Undefined]"; - 如果实参是
null, 则返回"[object Null]"; - 把实参转成对象
- 获取对象的
Symbol.toStringTag属性值subType
- 如果
subType是个字符串,则返回[object subType] - 否则获取对象的
[[Class]]属性值type,并返回[object type]
四、instanceof
4.1 语法
object instanceof constructorFunc
instanceof 操作符判断构造函数constructorFunc的prototype属性是否在对象object的原型链上。
Object.create({}) instanceof Object // true
Object.create(null) instanceof Object // false
Function instanceof Object // true
Function instanceof Function // true
Object instanceof Object // true
- 作为类型判断的一种方式,
instanceof操作符不会对变量object进行隐式类型转换
"" instanceof String; // false,基本类型不会转成对象
new String('') instanceof String; // true
- 对于没有原型的对象或则基本类型直接返回
false
1 instanceof Object // false
Object.create(null) instanceof Object // false
-
constructorFunc必须是个对象。并且大部分情况要求是个构造函数(即要具有prototype属性)
// TypeError: Right-hand side of 'instanceof' is not an object
1 instanceof 1
// TypeError: Right-hand side of 'instanceof' is not callable
1 instanceof ({})
// TypeError: Function has non-object prototype 'undefined' in instanceof check
({}) instanceof (() => {})
4.2 intanceof的缺陷
不同的全局执行上下文的对象和函数都是不相等的,所以对于跨全局执行上下文intanceof就不能正常工作了。
<!DOCTYPE html>
<html>
<head></head>
<body>
<iframe src=""></iframe>
<script type="text/javascript">
var iframe = window.frames[0];
var iframeArr = new iframe.Array();
console.log([] instanceof iframe.Array) // false
console.log(iframeArr instanceof Array) // false
console.log(iframeArr instanceof iframe.Array) // true
</script>
</body>
</html>
4.3 原理
Symbol.hasInstance函数
instanceof操作符判断构造函数constructorFunc的prototype属性是否在对象object的原型链上。但是可以利用Symbol.hasInstance自定义instanceof操作逻辑。
var obj = {}
// 自定义Symbol.hasInstance方法
Object.defineProperty(obj, Symbol.hasInstance, {
value: function() {
return true;
}
});
1 instanceof obj // true
当然了这个举例没有任何实际意义。只是说明下Symbol.hasInstance的功能。Symbol.hasInstance本意是自定义构造函数判断实例对象的方式,不要改变instanceof 的含义。
原型链
4.4 instanceof内部逻辑
综上可以梳理instanceof内部逻辑
object instanceof constructorFunc
- 如果
constructorFunc不是个对象,或则是null,直接抛TypeError异常; - 如果
constructorFunc[Symbole.hasInstance]方法,则返回!!constructorFunc[Symbole.hasInstance](object ) - 如果
constructorFunc不是函数,直接抛TypeError异常; - 遍历
object的原型链,逐个跟constructorFunc.prototype属性比较:
- 如果
object没有原型,则直接返回false; - 如果
constructorFunc.prototype不是对象,则直接抛TypeError异常。
五、内置的类型判断方法
5.1 Array.isArray
ES5引入了方法Array.isArray专门用于数组类型判断。Object.prototype.toString和instanceof都不够严格
var arr = []
Object.defineProperty(Array.prototype, Symbol.toStringTag, {
get() {
return 'myArray'
}
})
console.log(Object.prototype.toString.call(arr)); // [object myArray]
console.log(Array.isArray(arr)); // true
console.log(Array.prototype instanceof Array); // false
console.log(Array.isArray(Array.prototype)); // true
不过现实情况下基本都是利用Object.prototype.toString作为Array.isArray的polyfill:
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
六、内置对象的prototype属性类型判断
内置的对象Number, String, Boolean, Object, Function, Date, RegExp, Array都是各自类型对象的构造函数,并且他们的prototype属性都是各自实例对象的原型。但是这些内置对象的prototype属性又是什么类型呢?
6.1 Number.prototype
Number.prototype也是个数字,类似Number(0),但是Number.prototype并不是Number的实例。
var prototype = Number.prototype
console.log(prototype == 0); // true
console.log(prototype instanceof Number); // false
console.log(Object.prototype.toString.call(protoype)); // [object Number]
6.2 String.prototype
String.prototype也是字符串,类似"",但是String.prototype并不是String的实例。
var prototype = String.prototype
console.log(prototype == ''); // true
console.log(prototype instanceof String); // false
console.log(Object.prototype.toString.call(prototype)); // [object String]
6.3 Boolean.prototype
Boolean.prototype也是Boolean,类似false,但是Boolean.prototype并不是Boolean的实例。
var prototype = Boolean.prototype
console.log(prototype == false); // true
console.log(prototype instanceof Boolean); // false
console.log(Object.prototype.toString.call(prototype)); // [object Boolean]
6.4 Object.prototype
Object.prototype也是Object,类似Object.create(null)的值(原型为null的空对象),但是Object.prototype并不是Object的实例。
var prototype = Object.prototype
Object.getPrototypeOf(prototype); // null
console.log(prototype instanceof Object); // false
console.log(Object.prototype.toString.call(prototype)); // [object Object]
6.5 Function.prototype
Function.prototype也是Function,是个空函数,但是Function.prototype并不是Function的实例。
var prototype = Function.prototype
console.log(prototype()) // undefined
console.log(prototype instanceof Function); // false
console.log(Object.prototype.toString.call(prototype)); // [object Function]
6.6 Array.prototype
Array.prototype也是Array,是个空数组,但是Array.prototype并不是Array的实例。
var prototype = Array.prototype
console.log(prototype instanceof Array); // false
console.log(Array.isArray(prototype)) // true
console.log(Object.prototype.toString.call(prototype)); // [object Array]
6.6 RegExp.prototype
RegExp.prototype并不是RegExp的实例。但是关于RegExp.prototype是RegExp还是对象存在兼容性问题,有些浏览器下RegExp.prototype也是RegExp,并且是个总返回true的正则。
var prototype = RegExp.prototype
console.log(prototype.test('hello')) // true
console.log(prototype instanceof RegExp); // false
// Chrome v84返回"[object Object]", IE返回"[object RegExp]"
console.log(Object.prototype.toString.call(prototype)); //