偶然看到一篇文章,抖音前端安全团队的 一名【合格】前端工程师的自检清单,但文章看起来其实更像劝退表。看完以后发现其实自己只是会写前端罢了,很多问题都没有追问过为什么。
同时这篇文章也包含了一些前端进阶的点,正好也是自己需要的,所以就跟着这份清单从头梳理一遍前端内容,为自己的查漏补缺,也看看我能坚持多久,为了能有动力坚持更久,希望各位看官不要吝啬手里的赞和评论。
关于数据类型
动态类型: JS是弱类型或者说动态语言,即不需要提前声明变量类型
数据类型
原始类型/基本类型:可以用typeof运算符获取类型
- undefined:没有被赋值的变量的默认值
- Boolean:
true和false - Number
- String
- BigInt
- Symbol
- null
Number
范围:基于 IEEE 754 标准的双精度 64 位二进制格式的值 -(2^53-1) 到 2^53-1
安全范围校验:Number.MAX_VALUE 和 Number.MIN_VALUE;Number.isSafeInteger()、Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER
包含:整数、浮点数、+Infinity、-Infinity、NaN
BigInt
基础的数值类型,可以用任意精度表示整数。 可以使用运算数运算,也可以转换成Boolean,但不能和数字进行运算
Symbol
通过Symbol([description])创建,description参数仅可用于调试。
Symbol可以用于创建匿名的对象属性,创建类的私有成员。
用Symbol创建的属性不可枚举,因此不会在循环结构(比如for in)中出现;因为创建的属性是匿名的,因此也不能通过Object.getOwnPropertyNames()获取。但是可以通过创建时的原始Symbol访问,或者使用Object.getOwnPropertySymbols()获取到Symbol属性的数组。
Null 和 Undefined
- Null和Undefined两种类型都只有一个值,分别就是null和undefined
- null值表示空对象指针,所以
typeof null === 'object' - undefined值是由null派生而来的,所以
null == undefined // true,但仅限于==,===的话是false - null应该给未赋值的对象类型变量填充使用,而不必将未赋值的原始值类型变量显示的设置为undefined
复杂类型/引用类型
Object
在计算机科学中,对象是指内存中的可以被标识符引用的一块区域。 ---MDN
ECMAScript定义的对象属性有两类:数据属性和访问器属性,数据属性就是我们自定义的属性,访问器属性是get和set。
关于存储
关于变量存储 资料观点相悖,目前还没有找到权威资料,待补充
原始值和引用值(值类型和引用类型)
原始值:最简单的数据,也就是上边提到的原始类型。原始值我们操作的直接就是存储在变量中的实际数据,因此是按值访问(所以叫值类型)。
引用值:由多个值构成的对象。引用值是保存在内存中的对象,但是JS并没有直接访问内存位置的能力,所以保存在变量中的只是对对象数据的引用(内存地址),因此是按引用访问。
复制操作
原始值和引用值变量存储方式的不同,也决定了在进行变量复制时不同的表现。
原始值通过变量复制给另一个变量时,会把值复制一份,两份互不干扰。借用红宝书的图示:
引用值变量因为是存储的对象在内存中的地址,所以在复制操作中,新变量拿到的也只是一份内存地址。这就导致两个变量指向的其实是同一个对象,所以在修改过程中会相互影响,这也就涉及到了深拷贝和浅拷贝。同样借用红宝书图示:
关于原始值包装类型
原始值包装类型,也就是基本类型对应的内置对象,一共有三种:Boolean、Number、String。
当我们定义一个字符串后,可以通过 .length 获取字符串的长度,还可以通过 .includes() 判断字符串中是否包含某些字符等等。
但是基本类型并不是对象,应该不存在属性和方法,然而我们使用的时候并不会报错,这是因为JS在背后默默地为我们做了很多。
在我们读取一个基本类型时,JS会帮我们做下面写着操作
// 定义一个字符串
let str = 'I am string';
// 这是我们操作str的写法
let res = str.includes('str');
// 这是实际JS帮我们完成的事(简单理解版)
let str = new String("I am string");
let res = str.includes('str');
str = null;
所以可以看出来,当我们读取原始值的属性或方法时,JS会先通过内置对象创建一个对应类型的实例,然后访问实例上我们需要的属性或方法,调用结束以后销毁创建的实例。
判断数据类型
// 基本类型
let num = 1
let str = 'javascript'
let bool = true
let bignum = 1n
let unde = undefined
let nul = null
let sym = Symbol('javascript')
// 引用类型
let obj = { lang: 'javascript', version: 'ES6' }
let func = function() { console.log('javascript') }
let arr = ['1', '2', 3]
typeof
typeof num // 'number'
typeof str // 'string'
typeof bool // 'boolean'
typeof bignum // 'bigint'
typeof unde // 'undefined'
typeof nul // 'object'
typeof sym // 'symbol'
typeof obj // 'object'
typeof func // 'function'
typeof arr // 'object'
总结:可以判断除 Null 以外的基本类型以及 函数。
instanceof
用于检测构造函数的prototype属性(constructor.prototype)是否出现在某个实例对象的原型链上。
A instanceof B,就是判断B的构造函数的原型是否在A的原型链上。用它检测类型其实就是判断JS内置的那些类型构造函数的原型在不在某个变量的原型链上,也就是如下图圈起来的那部分关系:因为Object的prototype属性在实例对象o的原型链上,所以o是object类型。
num instanceof Number // false
str instanceof String // false
bool instanceof Boolean // false
bignum instanceof BigInt // false
// 因为Undefined和Null两个基本类型没有对应的内置对象,所以无法使用instanceof判断
sym instanceof Symbol // false
obj instanceof Object // true
func instanceof Function // true
arr instanceof Array // true
总结:instanceof不能用来检测直接声明的基本类型,但是如果使用new 构造函数的方式声明,则可以检测。它可以检测引用类型,但需要注意的是,要把 instanceof Object的判断放在最后,因为Function 和 Array的 instanceof Object也是true。
constructor
可以返回一个实例对象的构造函数。
num.constructor === Number // true
str.constructor === String // true
bool.constructor === Boolean // true
bignum.constructor === BigInt // true
// Null 和 Undefined 没有 constructor 属性
sym.constructor === Symbol // true
obj.constructor === Object // true
func.constructor === Function // true
arr.constructor === Array // true
总结:可以判断除Null和Undefined的其它类型,但因为constructor的指向可以改变,所以判断结果并不能确保准确。
Object.prototype.toString
当toString()在自定义对象中没有被覆盖时,返回"[object type]",type是对象的类型。
为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg。
Object.prototype.toString.call(num) // '[object Number]'
Object.prototype.toString.call(str) // '[object String]'
Object.prototype.toString.call(bool) // '[object Boolean]'
Object.prototype.toString.call(bignum) // '[object BigInt]'
Object.prototype.toString.call(unde) // '[object Undefined]'
Object.prototype.toString.call(nul) // '[object Null]'
Object.prototype.toString.call(sym) // '[object Symbol]'
Object.prototype.toString.call(obj) // '[object Object]'
Object.prototype.toString.call(func) // '[object Function]'
Object.prototype.toString.call(arr) // '[object Array]'
总结:看起来最完美的判断方法,常用类型都可以准确判断
类型转换
显式类型转换
手动发起的类型转换,主要就是toString,toNumber,toBoolean。
- toString
| 原值类型 | 转换后 |
|---|---|
| String | 原值 |
| Number | '1', 'NaN', 'Infinity' |
| Boolean | 'true', 'false' |
| Undefined | 'undefined' |
| Null | 'null' |
| Symbol | VM1086:1 Uncaught TypeError: Cannot convert a Symbol value to a string |
| BigInt | 丢掉n之后的String |
| Object | '[Object type]' |
- toNumber
| 原值类型 | 转换后 |
|---|---|
| Number | 原值 |
| String | 1. 纯数字字符串:转为对应的数字 2. 空字符串:转为0 3. |
| Boolean | true: 1 ; false: 0 |
| Undefined | NaN |
| Null | 0 |
| Symbol | Uncaught TypeError: Cannot convert a Symbol value to a number |
| BigInt | Uncaught TypeError: Cannot convert a BigInt value to a number |
| Object | NaN |
- toBoolean
| 原值类型 | 转换后 |
|---|---|
| Boolean | 原值 |
| String | 空字符串为false,其余为true |
| Number | 0 和 NaN为false,其余为true |
| Undefined | false |
| Null | false |
| Symbol | true |
| BigInt | true |
| Object | true |
隐式类型转换
隐式类型转换是在尝试操作一个“错误”的数据类型时,JS会自动转换为“正确的”数据类型,一般会会发生在通过运算符进行运算时。
原始类型变量的隐式转换也和上述手动转换操作结果一样,至于对象变量的转换,我们看一下ECMAScript官方规范的说明:
ToPrimitive
The abstract operation ToPrimitive takes an input argument and an optional argument PreferredType. The abstract operation ToPrimitive converts its input argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favour that type. Conversion occurs according to the following algorithm:
翻译:隐式操作
ToPrimitive接收一个参数input和一个可选参数PreferredType。ToPrimitive将它接收到的参数input转换为一个非对象类型。如果一个对象能够转换为多种原始类型,它可以使用提示参数PreferredType所给的类型。转换根据以下算法进行:
1. Assert: input is an ECMAScript language value.
// 1. 断言:参数input是ECMAScript的值
2. If Type(input) is Object, then
// 2. 如果input是Object
a. If PreferredType is not present, let hint be "default".
// a. 如果 PreferredType 参数不存在,hint为 "default"
b. Else if PreferredType is hint String, let hint be "string".
// b. 如果 PreferredType 是 hint String, hint为 "string"
c. Else,
// c. 如果都不是
1. Assert: PreferredType is hint Number.
// 1. PreferredType则为 hint Number
2. Let hint be "number".
// 2. hint 为 "number"
d. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
// d. 把 input参数的 @@toPrimitive 方法赋值给exoticToPrim
e. If exoticToPrim is not undefined, then
// e. 如果exoticToPrim不是 undefined
1. Let result be ? Call(exoticToPrim, input, « hint »).
// 1. 把 Call(exoticToPrim, input, « hint ») 的结果赋值给 result,也就是执行 exoticToPrim(input, « hint »)
2. If Type(result) is not Object, return result.
// 2. 如果result不是对象类型,则 return result
3. Throw a TypeError exception.
// 3. 否则抛出错误
f. If hint is "default", set hint to "number".
// f. 如果hint是default,把它重置为number
g. Return ? OrdinaryToPrimitive(input, hint).
// g. 返回OrdinaryToPrimitive(input, hint)的执行结果
3. Return input.
// 3. 如果input不是对象类型,直接返回原值
When ToPrimitive is called with no hint, then it generally behaves as if the hint were Number. However, objects may over-ride this behaviour by defining a @@toPrimitive method. Of the objects defined in this specification only Date objects (see 20.4.4.45) and Symbol objects (see 19.4.3.5) over-ride the default ToPrimitive behaviour. Date objects treat no hint as if the hint were String.
当在没有hint值的时候调用
ToPrimitive,通常会把hint当做Number。但是有些对象类型会通过@@toPrimitive方法重写这个行为。当前在ECMAScript规范中只有Date和Symbol会重写默认行为。Date在没有hint值的时候会默认当做String。
OrdinaryToPrimitive
在ToPrimitive方法中,如果input参数没有特殊的@@toPrimitive方法,最后会返回OrdinaryToPrimitive(input, hint)的执行结果。现在我们来看这个方法干了些啥:
1. Assert: Type(O) is Object.
// 1. 输入O是对象类型
2. Assert: Type(hint) is String and its value is either "string" or "number".
// 2. hint是字符串类型,并且它的值是 "string" 或者 "number"
3. If hint is "string", then
// 3. 如果 hint 是 "string"
a. Let methodNames be « "toString", "valueOf" ».
// a. 把 methodNames 设置为 « "toString", "valueOf" »
4. Else,
// 4. 否则
a. Let methodNames be « "valueOf", "toString" ».
// a. methodNames 设置为 « "valueOf", "toString" »
5. For each name in methodNames in List order, do
// 5. 按顺序遍历methodNames中的值为name
a. Let method be ? Get(O, name).
// a. 把 method 设置为 O[name],也就是获取O上的name方法
b. If IsCallable(method) is true, then
// b. 如果method可以被调用
1. Let result be ? Call(method, O).
// 1. 把 method 的执行结果赋值给 result
2. If Type(result) is not Object, return result.
// 2. 如果 result 不是对象,则返回 result
6. Throw a TypeError exception.
// 否则,抛出错误
隐式类型转换的规则可以总结为:
- 先判断传入的值是否为对象,如果不是对象类型则直接返回原值。
- 如果是对象的话,判断是否存在PreferredType参数,不存在的话将hint设置为‘default’,如果是string,则hint为string,如果是其它值,hint一律设置为‘number’。
- 判断input参数上是否有
@@toPrimitive,有的话调用该方法,如果结果不是对象,则返回结果;否则抛出类型错误。 - 将hint设置为number。
- 如果hint是string,就先调用
toString,后调用valueOf。 - 否则,先调用
valueOf,后调用toString。 - 如果某个方法调用结果不是对象,就返回调用结果
- 否则抛出错误
至于涉及到运算符的时候,是哪种类型妥协为另一种类型,就要看具体运算符的规范了,比如 数字和字符串相加,数字就会被转化为字符串;但是相减,字符串又会转换为数字。大家可以自行查看ECMAScript规范,如果呼声高,回头专门安排一篇。
关于JS精度
JS遵循IEEE 754标准,采用双精度(64位)存储数字。这64位包括1位符号位,11位指数位,52位小数位。
那计算机是如何存储一个数字的?以32.25为例,我们来按照规则转换试试:
- 首先把32.25转换为二进制,得到
100000.01 - 然后把二级制通过科学计数法表示,得到1.0000001*2^5^
- 符号位:正数为0,负数为1,所以符号位为
0 - 指数位:表示指数域的编码值,等于指数的实际值(也就是这里的5)加上某个固定的值(标准规定固定值为2^e-1^-1,e是指数位的长度,在JS里是11,所以JS中固定值始终为1023)。由此计算可得32.25的指数位是1028
10,将结果转换为二进制得到10000000100 - 小数位:即0000001,需要用0补足52位
所以最终得到32.25的存储形式为:
0 10000000100 0000001000000000000000000000000000000000000000000000
精度丢失的问题
我们知道了计算机内部是如何存储数的,那也就不难理解为什么会出现精度丢失的问题了。
我们以最经典的0.1 + 0.2 !== 0.3为例,先把0.1 和 0.2分别转成二进制得到:
0.0001 1001 1001 1001...(1001无限循环)
0.0011 0011 0011 0011...(0011无限循环)
但是因为双精度浮点数小数部分只有52位,所以剩余的部分就要参考十进制四舍五入的规则舍去,二进制中就是0舍1入,所以转换成浮点数表示得到:
0 01111111011 1001100110011001100110011001100110011001100110011010
0 01111111100 1001100110011001100110011001100110011001100110011010
因为在转成浮点数时涉及到了舍入,所以已经可以看出精度丢失了,这也是计算机中真正存储的数据。
做加法时计算机会先将存储的数据转化成二进制小数再相加,所以我们再将浮点数表示转化成二进制得到:
0.00011001100110011001100110011001100110011001100110011010
0.0011001100110011001100110011001100110011001100110011010
然后将二进制形式相加得到
0.01001100110011001100110011001100110011001100110011001110
将结果转化成十进制可以得到0.30000000000000004,到这里我们也就明白为什么0.1 + 0.2 !== 0.3了。
大号史莱姆危机
因为双精度浮点数小数位最大为52位,所以注定它所能表示的最大数也是有限的,很容易就可以想到,当全部52位都为1,而且没有小数点,那这时候这个数就应该是最大的。
因为科学计数法表示二进制数时,整数位总是1,所以在浮点数中这位是省略掉的,也就是说JS中最大数其实是53位二进制,当它们全为1时,得到的是9007199254740991。
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
当数字超过最大安全数时,就有可能会有舍入操作,所以就会出现精度丢失的情况。
9999999999999999 === 10000000000000001 // true 10000000000000000
将两个数转化成双精度浮点数表示后会发现两个数是一样的。
解决办法(当然要用元素反应啦)
-
对于小数的精度丢失问题,可以采用先放大运算再缩小结果的方式。至于放大倍数,可以根据实际数据小数位数动态决定。
-
对于大整数,目前有正处于Stage4阶段的BigInt属性,也可以通过转成字符串进行操作。
-
当然,不管是小数还是大整数,都可以借用风场(第三方库)的力量,比如mathjs等等。
最后
JS的变量和类型,比较重要的也就是以上这些了。
关于变量到底如何存储,因为在查询资料以后对之前一贯的观点:基础类型存在栈内存,引用类型存在堆内存的观点产生了质疑,但是目前还没有更权威的资料,ECMAScript标准也还没有肝到,所以就暂时搁置,后续找到以后补上。
能看到这里的都是大佬,希望各位大佬赏出手里的赞~