JS基础
# JS基础
# 原始类型
boolean null undefined number string symbol bigint
# null是Object吗?
虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
# 对象类型
常见面试题:
function test(person) {
person.age = 24
person = {
name: 'MA',
age: 22
}
return person
}
const p1 = {
name: 'Geroge',
age: 23
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
答案是:{'Geroge','24'} {'MA','22'}
- 首先,函数传参是传递对象指针的副本
- 到函数内部修改参数的属性这步,我相信大家都知道,当前
p1的值也被修改了 - 但是当我们重新为
person分配了一个对象时就出现了分歧 - 所以最后
person拥有了一个新的地址(指针),也就和p1没有任何关系了,导致了最终两个变量的值是不相同的。
# 原始类型与对象类型不同之处
对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
# typeof vs instanceof
typeof 对于原始类型来说,除了 null 会显示成object,其余都可以显示正确的类型
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型。
如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的,在后面的章节中我们也会自己去实现一个 instanceof。
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
对于原始类型来说,想直接通过 instanceof 来判断类型是不行的,但改一下 instanceof还是可以 判断原始类型的:
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
Symbol.hasInstance 是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。
这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
# 类型转换?
首先我们要知道,在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
# 转Boolean
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象。
# 对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是原始类型了,那就不需要转换了
- 如果需要转字符串类型就调用
x.toString(),转换为基础类型的话就返回转换的值。不是字符串类型的话就先调用valueOf,结果不是基础类型的话再调用toString - 调用
x.valueOf(),如果转换为基础类型,就返回转换的值 - 如果都没有返回原始类型,就会报错
当然可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
# 四则运算符
加法运算符不同于其他几个运算符,它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
如果你对于答案有疑问的话,请看解析:
- 对于第一行代码来说,触发特点一,所以将数字
1转换为字符串,得到结果'11' - 对于第二行代码来说,触发特点二,所以将
true转为数字1 - 对于第三行代码来说,触发特点二,所以将数组通过
toString转为字符串1,2,3,得到结果41,2,3
另外对于加法还需要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
因为 + 'b' 等于 NaN,所以结果为 "aNaN"。
对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
# 比较运算符
- 如果是对象,就通过
toPrimitive转换对象 - 如果是字符串,就通过
unicode字符索引来比较
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。
# 判断数组
var arr = []arr instanceof ArrayArray.prototype.isPrototypeOf(arr)arr.constructor === ArrayObject.prototype.toString.call(arr) === "[object Array]"Array.isArray(arr)Object.getPrototypeOf(arr) === Array.prototype
# for in、for of、forEach、map
map:数组方法,不改变原数组,返回新数组,可以使用break中断,可以return到外层函数
forEach:数组方法,不可以使用break中断,不可以return到外层函数
for in:用于遍历数组和对象,遍历对象键值(key),或者数组下标,不推荐循环一个数组
for of:不能遍历对象,需要通过和Object.keys()搭配使用。
for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议
for in循环出的是key,for of循环出的是value
# this
我们先来看几个函数调用的场景
function foo() { console.log(this.a)}var a = 1foo();const obj = { a: 2, foo: foo}obj.foo()const c = new foo()
接下来我们一个个分析上面几个场景
- 对于直接调用
foo来说,不管foo函数被放在了什么地方,this一定是window - 对于
obj.foo()来说,我们只需要记住,谁调用了函数,谁就是this,所以在这个场景下foo函数中的this就是obj对象 - 对于
new的方式来说,this被永远绑定在了c上面,不会被任何方式改变this
说完了以上几种情况,其实很多代码中的 this 应该就没什么问题了。
下面让我们看看箭头函数中的 this
function a() { return () => { return () => { console.log(this) } }}console.log(a()()())
首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 this 是 window。另外对箭头函数使用 bind这类函数是无效的。
以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。
首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
# ==与===
对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换。
假如我们需要对比 x 和 y 是否相同,就会进行如下判断流程:
首先会判断两者类型是否相同。相同的话就是比大小了
类型不相同的话,那么就会进行类型转换
会先判断是否在对比
null和undefined,是的话就会返回true判断两者类型是否是string和number,是的话就会将字符串转换为number
判断其中一方是否为boolean,是的话就会把boolean转为number再进行判断
判断其中一方是否object且另一方为string、number或者symbol,是的话就会把object转为原始类型再进行判断
现在来想想[]==![]输出什么?
首先先执行的是![],它会得到false。
然后[]==false,返回true。
那么[]==[],{}=={}又输出什么呢?
类型一致,它们是引用类型,地址是不一样的,所以为false!
对于 === 来说就简单多了,就是判断两者类型和值是否相同。
# 闭包
闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() { let a = 1 window.B = function () { console.log(a) }}A();B(); // 1
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
来看一道经典题目吧:
使用闭包解决var定义函数问题:
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000)}
因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
解决办法有三种,第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i);}
在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i )}
第三种就是**使用 let 定义 i **了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000)}
# 深浅拷贝
# 浅拷贝
首先可以通过 Object.assign 来解决这个问题,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址。
let a = { age: 1}let b = Object.assign({}, a)a.age = 2console.log(b.age) // 1
另外我们还可以通过展开运算符 ... 来实现浅拷贝
let a = { age: 1}let b = { ...a }a.age = 2console.log(b.age) // 1
通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了:
let a = { age: 1, jobs: { first: 'FE' }}let b = { ...a }a.jobs.first = 'native'console.log(b.jobs.first) // native
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。
# 深拷贝
这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。
let a = { age: 1, jobs: { first: 'FE' }}let b = JSON.parse(JSON.stringify(a))a.jobs.first = 'native'console.log(b.jobs.first) // FE
但是该方法也是有局限性的:
- 会忽略
undefined - 会忽略
symbol - 不能序列化函数
- 不能解决循环引用的对象
loader中使用deepclone可以解决
# 原型
每个 JS 对象都有 __proto__ 属性,这个属性指向了原型。这个属性在现在来说已经不推荐直接去使用它了,这只是浏览器在早期为了让我们访问到内部属性 [[prototype]] 来实现的一个东西。
原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于 obj 来说,可以通过 __proto__ 找到一个原型对象,在该对象中定义了很多函数让我们来使用。
原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性。
接下来就是经典的那张原型图登场了:

接下来,说一下原型链
原型链就是多个对象通过 __proto__ 的方式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数。
对于这一小节的知识点,总结起来就是以下几点:
Object是所有对象的爸爸,所有对象都可以通过__proto__找到它;Function是所有函数的爸爸,所有函数都可以通过__proto__找到它- 函数的
prototype是一个对象,仿函数所有实例化的对象们都可以找到公用的属性和方法 - 对象的
__proto__属性指向原型,__proto__将对象和原型连接起来组成了原型链 constructor属性的含义就是指向该对象的构造函数,所有函数最终的构造函数都指向Function。
# 事件循环
- 浏览器的时间循环
macro-task大概包括:
script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI render
micro-task大概包括:
process.nextTick、Promise、Async/Await(实际就是promise)、MutationObserver(html5新特性)
- Node的事件循环
macro-task 大概包括:
setTimeout、setInterval、setImmediate、script(整体代码)、I/O 操作等。
micro-task 大概包括:
process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)、new Promise().then(回调)等。