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(回调)等。