最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。敬请大家关注!
第一章: JS数据类型之问------概念章
JS原始数据类型有哪些?引用数据类型有哪些?
- 基本类型(值类型 原始值),在JS中存在着 7 种,分别是:
- undefined
- null
- number
- string
- boolean
- bigint
- symbol
- 引用数据类型(对象类型), 包含:
- Object 普通对象
- Function 函数对象
- Array 数组对象
- Date 日期对象
- Math 数学函数
- RegExp 正则对象
说出下面运行的结果,解释原因
javascript
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
/*
p1:{name: "fyq", age: 26}
p2:{name: "hzj", age: 18}
*/
原因: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,通过调用person.age = 26确实改变了p1的值,但随后person变成了另一块内存空间的地址,并且在最后将这另外一份内存空间的地址返回,赋给了p2
null是对象吗?为什么?
结论: null不是对象 解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object
'1'.toString()为什么可以调用?
其实在这个语句运行的过程中做了这样几件事情:
javascript
var s = new Object('1')
s.toString()
s = null
第一步: 创建Object类实例。注意为什么不是String ? 由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类。 第二步: 调用实例方法。 第三步: 执行完方法立即销毁这个实例。 整个过程体现了 基本包装类型 的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。 参考:《JavaScript高级程序设计(第三版)》P118
alert(6)输出的是字符'6',而不是数字6
java
alert(6)
//等同于
alert((6).toString())
0.1+0.2为什么不等于0.3?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失 ,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004 关于 js 中的浮点计算
那为什么typeof判断null为object?
:::warning 这是 JavaScript 语言的一个历史遗留问题,
- 在第一版JS代码中用32位比特来存储值,通过值的1-3位来识别类型,前三位为000表示对象类型。
- 而null是一个空值,二进制表示都为0,所以前三位也就是000,
- 所以导致 typeof null 返回 "object" :::
怎么判断为整数?
javascript
// 【推荐】 ES6提供了Number.isInteger
function isInteger6(num) {
return Number.isInteger(num);
}
console.log(isInteger6(Math.pow(2, 333))); // true
javascript
// 一、用typeof、取余% 判断
function isInteger1(num) {
return typeof num === 'number' && num % 1 === 0;
}
// 通过typeof操作符可以判断变量的类型,如果是number类型,那么就可以继续判断是否为整数。利用数字模1的余数是否等于0来判断是否为整数。如果余数等于0,则是整数,否则不是整数。
console.log(isInteger1(Math.pow(2, 333)), "isInteger1"); //true isInteger1
// 方式二、Math.round、Math.ceil、Math.floor判断
function isInteger2(num) {
return Math.floor(num) === num;
}
console.log(isInteger2(Math.pow(2, 333)), "isInteger2"); //true isInteger2
// 方式三、通过parseInt判断
function isInteger3(num) {
return parseInt(num, 10) === num;
}
console.log(isInteger3(1000000000000000000000), "isInteger3"); // false isInteger3
// 缺点:竟然返回了false,没天理啊。原因是parseInt在解析整数之前强迫将第一个参数解析成字符串。
// 而数字转字符超过21位后转成的是科学计数法
/*
(1000000000000000000000).toString()
'1e+21'
(100000000000000000000).toString()
'100000000000000000000'
*/
// 方式四、通过位运算判断
function isInteger4(obj) { return (obj | 0) === obj }
function isInteger4_1(obj) { return (obj << 0) === obj }
function isInteger4_2(obj) { return (obj >>> 0) === obj }
function isInteger4_3(obj) { return (~~obj) === obj }
// ~~参考 https://www.yuque.com/kenguba/js/irp0f68sv7nqy88p#ukIKU
// 但有个缺陷,上文提到过,位运算只能处理32位以内的数字,对于超过32位的无能为力,如
console.log(isInteger4(Math.pow(2, 32)), "isInteger4"); // false isInteger3
// 但有个缺陷,上文提到过,位运算只能处理32位以内的数字,处理起来有坑就有坑,如
/*
Math.pow(2, 30) << 0
-2147483648
1073741824
Math.pow(2, 31) << 0
-2147483648
-2147483648
Math.pow(2, 32) << 0
-2147483648
0
Math.pow(2, 33) << 0
-2147483648
Math.pow(2, 31) >>> 0
2147483648
Math.pow(2, 32) >>> 0
0
*/
console.log(isInteger4_1(Math.pow(2, 30)), "isInteger4_1"); // true isInteger4_1
console.log(isInteger4_2(Math.pow(2, 30)), "isInteger4_2"); // true isInteger4_2
// 五、用正则表达式判断
function isInteger5(num) {
return /^-?\d+$/.test(num);
}
console.log(isInteger5(Math.pow(2, 33))); // true
// 缺点:不适用于科学计数法,科学计数法过大用这个正则有坑,需要重写正则
/*
⚠️ 注意:32位存储的科学计数法中,1位符号位,8位指数位置,剩下23位位有效数字 ,所以科学计数法时候一定要注意
*/
如何理解BigInt?
什么是BigInt?
BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对 大整数 执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库
为什么需要BigInt?
在JS中,所有的数字都以双精度64位浮点格式表示,那这会带来什么问题呢? 这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991((2^53-1)),任何超出此范围的整数值都可能失去精度
javascript
console.log(999999999999999); //=>10000000000000000
同时也会有一定的安全性问题:
javascript
9007199254740992 === 9007199254740993; // → true 居然是true!
console.log(2 ** 53 - 1); //9007199254740991
console.log(9007199254740993); // 9007199254740992
console.log(9007199254740992 === 9007199254740993); // true
如何创建并使用BigInt?
要创建BigInt,只需要在数字末尾追加n即可
javascript
console.log( 9007199254740995n ); // → 9007199254740995n
console.log( 9007199254740995 ); // → 9007199254740996
另一种创建BigInt的方法是用BigInt()构造函数
javascript
BigInt("9007199254740995"); // → 9007199254740995n
简单使用如下:
javascript
10n + 20n // → 30n
10n - 20n // → -10n
+10n // → TypeError: Cannot convert a BigInt value to a number
-10n // → -10n
10n * 20n // → 200n
20n / 10n // → 2n
23n % 10n // → 3n
10n ** 3n // → 1000n
const x = 10n
++x // → 11n
--x // → 9n
console.log(typeof x) //"bigint"
值得警惕的点
- BigInt不支持一元加号运算符, 这可能是某些程序可能依赖于 + 始终生成 Number 的不变量,或者抛出异常。另外,更改 + 的行为也会破坏 asm.js代码。
- 因为隐式类型转换可能丢失信息,所以不允许在bigint和 Number 之间进行混合操作。当混合使用大整数和浮点数时,结果值可能无法由BigInt或Number精确表示。
javascript
10 + 10n // → TypeError
- 不能将BigInt传递给Web api和内置的 JS 函数,这些函数需要一个 Number 类型的数字。尝试这样做会报TypeError错误
javascript
Math.max(2n, 4n, 6n) // → TypeError
- 当 Boolean 类型与 BigInt 类型相遇时,BigInt的处理方式与Number类似,换句话说,只要不是0n,BigInt就被视为truthy的值。
javascript
if (0n) { } //条件判断为false
if (3n) { } //条件为true
- 元素都为BigInt的数组可以进行sort
- BigInt可以正常地进行位运算,如| & >> << ^
浏览器兼容性
caniuse的结果: 其实现在的兼容性并不怎么好,只有chrome67、firefox、Opera这些主流实现,要正式成为规范,其实还有很长的路要走。 我们期待BigInt的光明前途!
第二章: JS数据类型之问------检测章
typeof 是否能正确判断类型?
对于原始类型来说,除了 null 和 NaN 都可以调用typeof显示正确的类型 但对于引用数据类型,除了函数之外,都会显示"object"
javascript
typeof '' // string
typeof true // boolean
typeof 1 // number
typeof NaN // number
typeof undefined // undefined
typeof null // object
typeof Symbol() // symbol
typeof Function // function
typeof [] // object
typeof {} // object
因此采用typeof判断对象数据类型是不合适的,采用 instanceof 会更好,instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true
javascript
const Person = function () { }
const p1 = new Person()
p1 instanceof Person // true
var str1 = 'hello world'
str1 instanceof String // false
var str2 = new String('hello world')
str2 instanceof String // true
JS用来判断类型最好的方法是什么?
javascript
Object.prototype.toString.call('') // [object String]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(1) // [object Number]
Object.prototype.toString.call(NaN) // [object Number]
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(null) // [object Null]
Object.prototype.toString.call(Symbol()) // [object Symbol]
Object.prototype.toString.call([]) // [object Array]
Object.prototype.toString.call(new Function()) // [object Function]
Object.prototype.toString.call(new Date()) // [object Date]
Object.prototype.toString.call(new RegExp()) // [object RegExp]
Object.prototype.toString.call(new Error()) // [object Error]
Object.prototype.toString.call(window) // [object global] window 是全局对象 global 的引用
Object.prototype.toString.call(document) // [object HTMLDocument]
修改 Symbol.hasInstance 来判断基本数据类型
javascript
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
console.log(NaN instanceof PrimitiveNumber) // true
console.log("111" instanceof PrimitiveNumber) // false
如果你不知道Symbol,可以看看 MDN上关于hasInstance的解释 其实就是自定义instanceof行为的一种方式,这里将原有的instanceof方法重定义,换成了typeof,因此能够判断基本数据类型
能不能手动实现一下instanceof的功能?
⚠️ **注意:**字面量创建的用instanceof判断为false
javascript
console.log("" instanceof String) // false
console.log("111" instanceof String) // false
console.log(new String(111) instanceof String) // true
console.log(new String(NaN) instanceof String) // true
console.log(new String("111") instanceof String) // true
console.log(NaN instanceof Number) // false
console.log(111 instanceof Number) // false
console.log(new Number(NaN) instanceof Number) // true
console.log(new Number() instanceof Number) // true
核心: 原型链的向上查找
javascript
function instanceOf(left, right) {
if (typeof left !== 'object' || left === null) return false //基本数据类型直接返回false
let proto = left.__proto__
while (true) {
if (proto === null) return false
if (proto === right.prototype) { return true }
proto = proto.__proto__
}
}
console.log(instanceOf(new Date(), Date)); // true
console.log(instanceOf("", String)) // false
上面的 left.proto 这种写法可以换成 Object.getPrototypeOf(left)
javascript
function instanceOf(left, right) {
if (typeof left !== 'object' || left === null) return false //基本数据类型直接返回false
let proto = Object.getPrototypeOf(left) //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
while (true) {
if (proto == null) return false //查找到尽头,还没找到
if (proto == right.prototype) return true //找到相同的原型对象
proto = Object.getPrototypeOf(proto)
}
}
console.log(instanceOf("", String)) //false
console.log(instanceOf(new String(""), String)) //true
判空和比较是否相等
javascript
/**
* @description: 比较两个数据
* @return: Boolean
*/
/*
export const compare = (before: any, after: any): any => {
if (typeof before === "symbol" && typeof after === "symbol" ||
typeof before === "string" && typeof after === "string" ||
typeof before === "number" && typeof after === "number" ||
typeof before === "boolean" && typeof after === "boolean" ||
typeof before === "undefined" && typeof after === "undefined") {
if (typeof before === "number" && typeof after === "number" && isNaN(before) && isNaN(after)) return true
if (before === after) return true
return false
}
*/
if (Array.isArray(before) && Array.isArray(before)) {
if (before.length !== after.length) return false
return before.every((item: any, index: number) => compare(item, after[index]))
}
if (typeof before === 'object' && typeof after === 'object') {
if (before === after) return true
if (!before || !after) return false
if (Object.keys(before).length !== Object.keys(after).length) return false
return Object.keys(before).every((item: any) => compare(before[item], after[item]))
}
return false
}
//判断是否为空
export function isEmpty(params: any) {
if (isString(params)) { return params.length < 1 }
if (isPlainObject(params)) { return (Object.keys(params).length < 1) }
if (Array.isArray(params)) { return (params.length < 1) }
return true
}
//判断是不是对象
export function isPlainObject(val: any): val is Object {
return toString.call(val) === '[object Object]'
//return Object.prototype.toString.call(val) == "[object Object]"
//return typeof val === 'object' && !Array.isArray(val)
}
//判断是不是函数
export function isFn(val: any): val is Object {
return toString.call(val) === '[object Function]'
}
//判断是不是数组
export function isArray(val: any): val is Object {
return toString.call(val) === '[object Array]'
}
//判断是不是是不是字符串
export function isString(val: any): val is Object {
return typeof val === 'string'
// return toString.call(val) === "[object String]"
}
Object.is和===的区别?
Object在严格等于的基础上修复了一些特殊情况下的失误, 具体来说Onject.is()修复后就是+0 -0 不相等,NaN和NaN相等。 源码如下:
javascript
+0 === -0 //true
NaN === NaN //false
Object.is(+0, -0) //false
Object.is(NaN, NaN) //true
isNaN(NaN) //true
isNaN() //true
isNaN(undefined) //true
console.log(undefined == NaN); //false
isNaN(null) //false
isNaN("") //false
console.log(Infinity == Infinity); //true
console.log(+Infinity == - Infinity); //false
javascript
function is(x, y) {
if (x === y) {
// x和y都为(0,-0 , +0)时候
// 1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
//NaN === NaN是false,这是不对的,
//我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理两个都是NaN的时候返回true
return x !== x && y !== y;
}
}
console.log(is(+0, -0)); //false
console.log(is(NaN, NaN)); //true
console.log(is(+Infinity, -Infinity)); //false
第三章: JS数据类型之问------转换章
JS中类型转换只有三种
- 转换成字符串
- 转换成数字
- 转换成布尔值
== 和 ===有什么区别?
:::warning == 的比较规则: ① 相等操作符,不同数据类型会根据一定规则转换为同一数据类型再比较 ② 数组与数组比较,比较的是其引用 ③ 不同类型比较,遇数值则转数值,布尔值自身转数值 ④ 非空非零引用型,转为布尔均为真 ⑤ 与非null的Object做比较时候,会将Object转换成字符串,再进行比较
===的比较规则: 需要左右两边类型与值相等 ::: === 叫做严格相等,是指:左右两边不仅值要相等,类型也要相等
- 例如'1'===1的结果是false,因为一边是string,另一边是number
== 叫不严格相等,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:
- 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
- 判断是否是null和undefined,是的话就返回true
- 判断类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
- 判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
- 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
- 都是对象的时候,比较的是引用
javascript
console.log({a: 1} == true); //false
console.log({a: 1} == "[object Object]"); //true
[] == [] 结果为什么是false?
简单回答:数组的所在的地址不同 在JS中,数组是属于引用型数据类型,所以"=="左右两边所表示的实际只是数组的所在的地址而已。在创建一个新数组时,其地址均不相同,因此[]==[]最终返回false。
javascript
console.log([] == 0) // true
console.log([] == []) // false
console.log([] === 0) // false
console.log([] === []) // false
[] == ![] 结果为什么是true?
javascript
console.log([] == ![]); // true
console.log([0] == ![0]); // true
console.log([0] == ![1]); // true
console.log([1] == ![0]); // false
console.log([1] == ![1]); // false
console.log([] == []); // false
javascript
[] == ![] 最终转化为(0 == 0)后做比较
在 == 的运算中,左右两边都需要转换为数字然后进行比较时,
[]虽然作为一个引用类型转换为布尔值为 true,但是作为比较的时候:
① 将==右侧数组转换为布尔值后再取反,转换后相当于[]==false
② 根据"比较前布尔值转数值"规则,转换后相当于[]==0
③ 根据"比较前数组遇数值先转字符串后转数值"规则,转换后相当于0==0
右边:![] => false,
console.log(![]); // false
左边: []为空数组 => 转换为数字为 0
console.log(Number([])); // 0
(0 == false) =>(0 == 0)
对象转原始类型是根据什么流程运行的?
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:
- 如果有Symbol.toPrimitive()方法,优先调用再返回
- 调用valueOf(),如果转换为原始类型,则返回
- 调用toString(),如果转换为原始类型,则返回
- 如果都没有返回原始类型,会报错
javascript
var obj = {
value: 1,
[Symbol.toPrimitive]() {
return 2
},
valueOf() {
return 3
},
toString() {
return '4'
},
}
console.log(obj + 1) // 输出3
console.log(obj.value + 1) // 输出2
console.log(obj.valueOf() + 1) // 输出4
console.log(obj.toString() + 1) // 输出41
如何让if(a == 1 && a == 2)条件成立?
其实就是上一个问题的应用
javascript
var a = {
value: 0,
[Symbol.toPrimitive]() {
++this.value;
return this.value;
}
};
console.log(a == 1 && a == 2); //true
console.log(parseInt(a)); //3
console.log(parseInt(a)); //4
javascript
var a = {
value: 0,
valueOf: function () {
this.value++;
return this.value;
},
};
console.log(a == 1 && a == 2); //true
console.log(a); // { value: 2, valueOf: [Function: valueOf] }
console.log(a); //{ value: 2, valueOf: [Function: valueOf] }
console.log(a == 3); //true
参考文献
:::info 基础篇(1) :::