前言
学习了这么久前端,发现自己对于基础知识的掌握并没有那么通透,于是打算重新学一遍JS,引用经济学的一句话:JS基础决定能力高度🤦🏻
基础很重要,只有基础好才会很少出 bug,大多数的 bug 都是基础不扎实造成的
JS代码如何编译
众所周知,JavaScript是一门“动态”或“解释执行”语言,也就说它不会提前编译,而是在运行的前几微秒去进行编译的,那么我们来看看它是如何编译的
假如我们就去编译这一段简单的代码
const a = 12;
1. 分词/词法分析
这个过程会将由字符组成的字符串分解为有意义的代码块,这些代码块称为词法单元,比如上面的代码会被分为这些词法单元:const、 a、=、12、;。
你可能会问空格不会被作为词法单元吗?
=
号前后的空格其实并不影响代码运行,空格在JS里面并没有太大意义
词法分析与分词的区别在于对词法单元的识别是有状态还是无状态的方式进行的,比如词法单元a是一个独立的词法单元还是其他词法单元的一部分
比如let a = 12; a = 20;
前者是分词,后者就是词法分析
2. 解析/语法分析
这个过程就是将词法单元流(数组)转化为“抽象语法树(AST)”
不熟悉的同学可以去astexplorer玩玩
3. 代码生成
将AST转化为可执行代码的过程就叫做代码生成,具体方法就是将const a = 12;
的AST转化为一组机器指令,用来创建一个a的变量(包括分配内存等),并将值保存在a中
有兴趣的同学可以去看看这个最小的编译器the-super-tiny-compile
编译的过程会拿到a变量名,以及对应赋值的类型,那么我们接下来看看都有哪些类型
JS的数据类型
JavaScript
中共有七种内置数据类型,包括基本类型
和引用类型
基本类型有以下6种:
- Number(数字)
- String(字符串)
- Boolean(布尔)
- Symbol(符号)(ES2015 新增)
- null(空)
- undefined(未定义)
注意⚠️:
String
、Number
、Boolean
和null
undefined
这五种类型统称为原始类型(Primitive),表示不能再细分下去的基本类型Symbol
是唯一并且不可变的原始值并且可以用来作为对象属性的键,symbol 的目的是去创建一个唯一属性键,保证不会与其它代码中的键产生冲突。undefined
表示没有任何值,null
表示没有任何对象,当某些东西没有值时,该语言通常默认为undefined
引用类型
- Object(对象)
- Function(函数)
- Array(数组)
- Date(日期)
- RegExp(正则表达式)
引用类型顾名思义就是真实存储的是一个引用地址,与基本类型不同的是,对象是唯一可变的值,同时对象在逻辑上是属性的无序集合,是存放各种值的容器
基本类型与引用类型的异同?
1. 存放位置
基本类型的值在内存中的大小是固定的,并且是直接存储在栈内存
引用类型在栈内存里存储了对应的指针地址,真实的数据存储在堆内存
2. 值的可变性
基本类型的值是不可变
的
let name = 'test'
name.toUpperCase(); // TEST
console.log(name); // 'test'
name[1] = 'y';
console.log(name); // 'test'
引用类型的值是可变
的
const arr = [1, 2, 3];
arr[0] = 6;
console.log(arr); // [6, 2, 3]
3. 比较
基本类型的比较是值
的比较
const a = 1;
const b = true;
console.log(a === true); // false
console.log(a == true); // true
看到这里你可能会问,为啥值明明不同但是用==
它们相等了啊,这里就涉及到了隐士类型转换的知识了,后面我们再讲
引用类型的比较是地址
的比较
const str1 = '{}';
const str2 = '{}';
const obj1 = {};
const obj2 = {};
console.log(str1 === str2); // true
console.log(obj1 === obj2); // false
对象直接赋值会影响原对象?
const obj = {name: 'test'};
const obj1 = obj;
obj1.name = 'test1';
console.log(obj); // {name: 'test1'}
我们已经知道对象是引用类型,指向的是引用地址
,如果直接赋值的话,那么赋值的是引用地址,所以如果修改obj1.name,其实也就是在修改obj1指向的堆内存的内容
1. number类型
JavaScript 采用 “遵循 IEEE 754 标准的双精度 64 位格式”表示数字,当然也会有精度超出的情况,于是出现了一下临界值
Number.MIN_VALUE
: 表示在 JavaScript 中所能表示的最小的正值
Number.MAX_VALUE
: 表示在 JavaScript 里所能表示的最大数值
Number.MAX_SAFE_INTEGER
:表示最大的安全整数(2^53 - 1)
Number.MIN_SAFE_INTEGER
: 代表在 JavaScript 中最小的安全的 integer 型数字 (-(2^53 - 1))
Infinity
的初始值是 Number.POSITIVE_INFINITY
。Infinity
(正无穷大)大于任何值
-Infinity
的初始值是 Number.NEGATIVE_INFINITY
。-Infinity
(负无穷大)小于任何值
NaN
: 是一个特殊种类的数值,当算术运算的结果不表示数值时,通常会遇到它。它也是 JavaScript 中唯一不等于自身的值
常见问题:0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?
这是浮点数精度问题,都是由于浮点数无法精确表示引起的,因为运算过程先按照 IEEE 754 转成相应的二进制,然后对阶运算,具体原因和解决可以看这篇0.1 + 0.2 不等于 0.3?为什么 JavaScript 有这种 “骚” 操作?
常用函数:
- parseInt:将
字符串转换为整型
,支持多种进制转换
parseInt("010", 10); // 10
parseInt("010", 8); // 8
parseInt("11", 2); // 3
- parseFloat:用以
解析浮点数字符串
,只应用于解析十进制数字
parseFloat('3.14'); // 3.14
parseFloat("FF2"); // NaN
parseInt() 和 parseFloat() 函数会尝试逐个解析字符串中的字符,直到遇上一个无法被解析成数字的字符,然后返回该字符前所有数字字符组成的数字。但是运算符 “+” 对字符串的转换方式与之不同,只要字符串含有无法被解析成数字的字符,该字符串就将被转换成 NaN
- toFixed: 使用定点表示法来格式化一个数值
常见问题:1.335.toFixed(2)为1.33
javascript使用 64用位双精度浮点型
来表示数字,想用有限的位来表示无穷的数字,这显然不可能,因此会出现精度问题,这其实也是因为浮点数无法精确表示原因造成的
解决方案可以看菜鸟教程
2. string类型
String 类型表示文本数据,字符串中的每个元素在字符串中占据一个位置。第一个元素的索引为 0,下一个是索引 1,依此类推,字符串的长度是它的元素的数量。
字符串是不可变的,这意味着你一旦创建,就不可能修改它,字符串的方法会基于当前的字符串的内容创建一个新的字符串常见方法有substr
、concat
、slice
等
通常表示文本数据时候推荐使用字符串。当需要表示复杂的数据时,使用字符串解析并使用适当的抽象。
JSON.stringify
: 将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性
JSON.stringify(value[, replacer [, space]])
space是指定缩进用的空白字符串,用于美化输出
function replacer(key, value) {
if (typeof value === "string") {
return undefined;
}
return value;
}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);
// {"week":45,"month":7}
当然也会有一些问题,使用时需要注意一下
3. boolean类型
boolean
类型表示一个逻辑实体并且包括两个值:true 和 false
布尔值通常用于条件运算,包括三元运算符、if…else、while 等
真值表如下
注意⚠️:不要用创建 Boolean 对象的方式将一个非布尔值转化成布尔值,直接将 Boolean 当做转换函数来使用即可,或者使用双重非(!!)运算符
4. symbol类型
Symbol
是 ES6 中引入的新类型,是唯一并且不可变的原始值并且可以用来作为对象属性的键,symbol
的目的是去创建一个唯一属性键,保证不会与其它代码中的键产生冲突。
//创建
const mySymbol = Symbol("my symbol");
console.log(typeof mySymbol); // "symbol"
const obj = {[mySymbol]: 1};
obj[mySymbol]; // 1
5. null 类型
null
类型只有一个值:null,null 是表示缺少的标识,指示变量未指向任何对象。
使用场景:null 常在返回类型应是一个对象,但没有关联的值的地方使用。
常见问题: typeof null是object
// JavaScript 诞生以来便如此
typeof null === "object";
这是因为 JS 的值是有一个表示类型的标签和实际数值表示的,对象的类型标签用0表示
,而 null 代表的是空指针,它的类型标签也是0
,因此 typeof null 就返回了 ‘object’。
6. undefined 类型
undefined 类型只有一个值:undefined,undefined 表示没有任何值,null 表示没有任何对象,当某些变量没有值时
null
与undefined
的不同点:
- typeof 返回的类型不同
null
是一个关键字,而undefined
是一个普通的标识符,恰好是一个全局属性null == undefined
为true,这里因为==会发生强制类型转换,我们稍后说
7. object类型
在 JavaScript 中,对象可以被看作是一组属性的集合,对象是唯一可变的值,函数也是具有额外可调用能力的对象
属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型
数据属性:
- value: 通过属性访问器获取值。可以是任意的 JavaScript 值
- writable:布尔值,用来表示是否可以通过赋值来改变属性
- enumerable: 布尔值,用来表示是否可以通过 for…in 循环来枚举属性
- configurable:布尔值,用来表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性
访问器属性:
- get: 该函数使用一个空的参数列表,以便有权对值执行访问时,获取属性值
- set: 使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行
- enumerable: 布尔值,用来表示是否可以通过 for…in 循环来枚举属性
- configurable:布尔值,用来表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性
对象属性调用两种调用(.属性和[属性])的区别:
强制类型转换
为什么会发生强制类型转换?
JavaScript 是一个弱类型语言。这意味着你可以经常使用一种类型的值,而另一种类型是预期的,并且该语言将为你转换它为正确的类型。为此,JavaScript 定义了少数强制规则
当然这里并不包括null
和undefined
,因为他两没有相应的对象包装器
当其他原始值访问属性时,JavaScript 会自动将值包装到相应的包装对象中,并访问对象上的属性,而在 null 或 undefined 上访问属性时,会抛出 TypeError 异常,这需要采用可选链运算符。
常见问题:JS 中强制类型转换是一个非常易出现 bug 的点,你知道强制转换时候的规则吗?
1. ToPrimitive强制原始值转换
在期望原始值的地方使用强制原始值转换的过程,这通常是当 字符串、数值或 BigInt 相同可以接受的时候。
- Date() 构造函数,当它收到一个不是 Date 实例的参数时 —— 字符串表示日期字符串,而数值表示时间戳。
+
运算符 —— 如果运算对象是字符串,执行字符串串联;否则,执行数值相加。==
运算符 —— 如果一个运算对象是原始值,而另一个运算对象是对象(object),则该对象将转换为没有首选类型的原始值。
ToPrimitive
对原始类型不发生转换处理,只针对引用类型(object)的,其目的是将引用类型(object)转换为非对象类型,也就是原始类型。
ToPrimitive
运算符接受一个可选的期望类型作参数。ToPrimitive
运算符将值转换为非对象类型,该函数被调用时,会被传递一个字符串参数 hint
,表示要转换到的原始值的预期类型。hint
参数的取值是 “number”、“string” 和 “default” 中的任意一个。
我们以Symbol.toPrimitive
为例看看
const object1 = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 42;
}
return null;
}
};
console.log(+object1); // 42
console.log('' + object1) // null
hint
不同值说明
- hint 为
string
- 先调用
obj
的toString
方法,如果为原始值,则return
,否则进行第 2 步 - 调用
obj
的valueOf
方法,如果为原始值,则return
,否则进行第 3 步 - 抛出
TypeError
异常
- hint 为
number
- 先调用
obj
的valueof
方法,如果为原始值,则return
,否则进行第 2 步 - 调用
obj
的tostring
方法,如果为原始值,则return
,否则进行第 3 步 - 抛出
TypeError
异常
- hint 为
default
- 该对象为
Date
,则hint
被设置为string
- 该对象为
symbol
,则忽略hint
,并总是返回一个symbol
- 否则,
hint
将走number
的逻辑
ToPrimitive
总结
ToPrimitive
强制转换为任意的原始类型时,取决于 hint
,hint
参数可选,若指定,则按照指定类型转换,若不指定,默认根据实用情况分两种情况,Date
为 string
,symbol
总是返回一个 symbol
, 其余对象走 number
的转换逻辑。
接下来我们看看另外两种转换方法。
2.valueOf强制原始值转换
Object.prototype.valueOf()
方法返回指定对象的原始值(一般有数值、字符串、布尔值),所有原始类型都有它们自己的 valueOf()
方法,并且在转换为原始值的过程中会自动调用(JavaScript 引擎在内部隐式调用的)
不同内置对象有不同的valueOf
的返回值:
- string -> 返回
String
对象的原始值 - number -> 返回一个被
Number
对象包装的原始值 - Date -> 返回一个
Date
对象的原始值 - boolean -> 返回
Boolean
对象的原始值 - object -> 返回
this
值,将其转换为一个对象 - symbol 对象无法隐式转换成对应的原始值
const str = new String('123');
const num = new Number(123);
const date = new Date();
const boolean = new Boolean(false);
const obj = new Object();
console.log(str.valueOf()) // "123"
console.log(num.valueOf()) // 123
console.log(date.valueOf()) // 1669613965429
console.log(boolean.valueOf()) // false
console.log(obj.valueOf()) // Object对象
3. toString强制原始值转换
toString()
方法返回一个表示该对象的字符串,所有原始类型都有它们自己的 toString()
方法,并且在转换为原始值的过程中会自动调用(JavaScript 引擎在内部隐式调用的)
不同内置对象有不同的toString
的返回值:
- string -> 返回
String
包装对象的字符串值 - number -> 返回指定
Number
对象的字符串表示形式 - Date -> 返回一个 表示给定
date
对象的字符串 - boolean -> 返回表示指定的布尔对象的字符串
- object -> 返回一个表示该对象的字符串
- array -> 返回一个字符串,表示指定的数组及其元素
- symbol -> 返回当前
symbol
对象的字符串表示
const x = new String("Hello world");
const count = 10;
const date = new Date();
const flag1 = new Boolean(true);
const obj = new Object();
const arr = [1, 2, 3];
const symbol = Symbol('foo');
console.log(x.toString()); // 'Hello world'
console.log(count.toString()); // '10'
console.log(date.toString()); // 'Mon Nov 28 2022 14:01:10 GMT+0800 (中国标准时间)'
console.log(flag1.toString()); // 'true'
console.log(obj.toString()) // "[object Object]"
console.log(arr.toString()); // "1,2,3"
console.log(symbol.toString()); // 'Symbol(foo)'
这里 Object 可以自己重写 toString 方法,不然的话会调用自身的 toString 方法,一般继承自 Object 的对象都继承了这个方法
Object.prototype.toString()
返回 “[object Type]”,这里的 Type 是对象的类型,所以可以用Object.prototype.toString()
去判断变量的类型
总结一下强制类型转换
如果值已经是原始值,则此操作不会进行任何转换。
如果是对象按以下顺序调用它的 @@toPrimitive(将 hint
作为 default
)、valueOf()
和 toString()
方法,将其转换为原始值。
原始值转换会在toString
之前调用valueOf
,这与hint
值为number
行为一致,但是hint
为string
却是先toString
然后才是valueOf
最后再详细讲解一下+
的转换流程,+
有两种不同的运算重载
:数字加法和字符串连接
- 先按照顺序(即
hint
为default
)将对象转换为原始类型 - 如果一方是字符串,则另一方也会被转换为字符串,执行两个字符串连接
- 如果双方都是
bigInt
类型,则执行bigInt
加法,如果一方是而另一方不是,则抛出TypeError
异常 - 否则,双方都会被转换为数字,执行数字加法
至于==
号,看下图「来自MDN」
因为我肯定是推荐大家用===
的,不进行类型转换,直接比较类型是否相同
数据类型判断
常见问题:如何判断一个值的数据类型呢?
在JS里面有三种判断数据类型的方式typeof
、instanceof
、Object.prototype.toString()
1. typeof
使用typeof
基本上可以判断出一个值属于哪种数据类型了
typeof 'test' // 'string'
typeof false // 'boolean'
typeof 100 // 'number'
typeof Symbol() // 'symbol'
typeof null // 'object' 这里无法判定是否为 null
typeof undefined // 'undefined'
typeof {} // 'object'
typeof [] // 'object'
typeof(() => []) // 'function'
从上面的结果可以来看:
null
类型判断错误,使用typeof
得到的结果是object
,具体原因上面有讲- 数组和对象的类型无法区分开来,它们返回的都是
object
,所以如果用了typeof
的话还得具体用Array.isArray
进行判断为佳
2. instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
console.log({} instanceof Object); // true
console.log([] instanceof Array); // trur
console.log((() => []) instanceof Function); // true
当然,instanceof
也不是万能的,同样会有问题存在
console.log({} instanceof Object); // true
console.log([] instanceof Object); // true
console.log((() => []) instanceof Object); // true
这是为啥呢?
因为arr
实例相当于new Array()
,也就是说arr.__proto__ === Array.prototype
,而Array
是继承自Object
的,即Array.prototype.__proto__ === Object.prototype
,所以当你直接用[] instanceof Object
的时候是能够通过原型链找到的
3. Object.prototype.toString()
Object.prototype.toString()
这个方法我们上面已经讲过了,Object
上的toString
方法会返回[object Type]
这个是判断类型的终极方案,判断十分准确
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(12)); // "[object Number]"
console.log(Object.prototype.toString.call('12')); // "[object String]"
console.log(Object.prototype.toString.call(false)); // "[object Boolean]"
console.log(Object.prototype.toString.call(Symbol())); // "[object Symbol]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
可以看到我们把所有的类型都测试了一遍,类型都是对的,这也是终极判断方法
可以尝试将其封装一下,就能得到判断类型的util函数了
function isType(value, type) {
const valueType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
return type === valueType;
}
console.log(isType(Symbol(), 'symbol')) // true
console.log(isType(12, 'number')) // true
console.log(isType({}, 'object')) // true
console.log(isType([], 'array')) // true
总结
最近梳理的前端知识点,希望同大家一起学习
本篇文章主要讲解了
- JS编译代码的过程
- 几种基本数据类型
- 基本类型与引用类型的异同
- 强制类型转换,推荐使用
===
代替==
- 类型判断的几种方式
如果对你有帮助的话,不妨点赞收藏关注一下,谢谢