一、数据类型检测
1.typeof
机制:直接在计算机底层基于数据类型的值(二进制)进行检测
typeof null: object 对象存储在计算机中,都是以000开头的二进制数存储,null也是,所以检测结果是object
typeof 0
number
-------------------------------------------
typeof 'abc'
string
-------------------------------------------
typeof true
boolean
-------------------------------------------
typeof undefined
undefined
-------------------------------------------
typeof null
object
-------------------------------------------
typeof {}
object
-------------------------------------------
typeof []
object
-------------------------------------------
typeof function(){}
function
- 作用:检测基本数据类型
- 局限:不能检测复杂对象类型
2.instanceof
由于typeof不能检测复杂对象类型,所以instanceOf弥补了这一局限。
机制:只要当前类的显式原型出现在实例的隐式原型链上,结果都位true
- 作用:检测当前实例是否属于这个类的
- 局限:1. 由于我们可以随意修改原型的指向,所以检测出来的结果不一定准确。2.不能检测基本数据类型
实现instanceof
这里也可以将递归改为while(true)
function myInstanceof(a, b){
if(a === null)
return false
if( a.__proto__ === b.prototype){
//实例的隐式原型指向构造函数的显式原型
return true
}else{
return myInstanceof(a.__proto__, b)
}
}
3.constructor
机制:实例的constructor属性指向构造函数
- 作用: 与instanceof类似,但可以检测基本数据类型
- 局限:constructor可以随意修改,所以也不准确
console.log( [].constructor === Array) true
let n = 1
console.log( n.constructor === Number) true
4. Object.prototype.toString.call()
- 作用:能检测所以数据类型
5.自定义类型检测函数
实现一个万能的检测函数依靠typeof和Object.prototype.toString.call,基本数据类型用typeof,对象数据类型用toString
// 数据类型映射表
let classType = {}
let types = ['Boolean', 'Number', 'String', 'Function', 'Array', 'Date', 'RegExp', 'Object', 'Error', 'Symbol']
.forEach(name => {
classType[`[object ${name}]`] = name.toLowerCase()
})
/*
检测类型函数
*/
function toType(obj) {
const toString = Object.prototype.toString
if(obj == null) {
return obj+'' //检测 null 和 undefined,返回字符串
}
if(typeof obj === 'object' || typeof obj === 'function'){
//对象类型用toString.call(obj)
return classType[ toString.call(obj) ]
}else {
//基本类型用typeof
return typeof obj
}
}
二、数据类型及转换
原始值类型:
- number 数字
- string 字符串
- boolean 布尔
- null 空对象指针
- undefined 未定义
- symbol 唯一值
- bigint 大数
对象类型:
- 标准普通对象:object
- 标准特殊对象:Array、RegExp、Date、Math、Error
- 非标准特殊对象:Number、String、Boolean
- 可调用\执行对象: Function
1. 其他类型 ---> Number
Number(val)
- 一般用于浏览器的隐式转换中
- 数学运算
- isNaN检测
- ==
规则:
- 字符串转变为数字,空字符串变为0,如果出现任何非有效数字字符的,都变为NaN
- true-->1, false-->0
- null-->0, undefined-->NaN
- Symbol无法转为数字
- BigInt去除'n' ,超过安全数的,会按科学计数法
- 对象转为数字: 先调用Symbol.toPrimitive这个方法,如果不存在这个方法,继续调用valueOf获取原始值,如果返回的不是原始值,再调用toString把其变为字符串,最后再把字符串基于Number方法转化为数字
parseInt(val,radix)
- 一般用于显示转换
规则:
val值一定要是字符串,如果不是则先转换为字符串;然后从字符串的左边第一个字符开始找,把找到的有效数字最后转换为数字【一个都没找到就是NaN】,遇到一个非有效数字的字符,则停止查找;parseFloat可以多识别一个小数点
2. 其他类型---> String
3. 其他类型---> Boolean
- 除了 0/NaN/空字符串/null/undefined转换为false,其他都是true
4. ==比较时候的相互转换规则
- 对象 == 字符串, 先将对象转换为字符串【Symbol.toPrimitive--> valueOf --> toString】,再比较
- null == undefined --> true, null/undefined和其他任何值都不相等
- 对象 == 对象,比较堆内存地址值
- NaN !== NaN , 但是可以用 Object.is(NaN, NaN)-->true
- 除了以上情况,只要两边类型不一致,都转换为数字,然后再进行比较
三、this的五种情况
- 方法调用,谁调用方法,this就指向谁
- 直接调用,非严格模式浏览器中this指向window,node中this指向globl,严格模式下this指向undefined
- new调用,this指向实例对象
- 事件绑定,this指向绑定的DOM对象
- call、apply、bind中this指向传入的context
手写call
/*
手写call方法
细节:
1. 要考虑是不是严格模式。如果是非严格模式,对于thisArg要特殊处理。
2. 如何判断严格模式?
3. thisArg被处理后还要进行非空判断,然后考虑是以方法的形式调用还是以普通函数的形式调用。
4. 目标函数作为方法调用时,如何不覆盖对象的原有属性?
5. thisArg如果为原始值,如何自动装箱
*/
Function.prototype.call = function (thisArgs, ...params) {
//待执行函数
let func = this
// 2. 如何判断严格模式?
const isStrict = (function(){
return this===undefined
})()
const isNull = (thisArgs==null)
const propName = Symbol('key')
let result
if(isNull){
//如果传入的thisArgs为空
if(isStrict){
//严格模式,this为undefined,则直接调用
result = func(...params)
}else{
//非严格模式,浏览器下this指向window,node环境指向globl
//如何判断环境
thisArgs = (function(){return this})
thisArgs[propName] = func
result = thisArgs[propName](...params)
delete thisArgs[propName]
}
}else{
//判断thisArgs是否为原始值,将原始值包装一下
const type = typeof thisArgs
if(type !== 'object' || type !== 'function')
thisArgs = Object(thisArgs)
thisArgs[propName] = func
result = thisArgs[propName](...params)
delete thisArgs[propName]
}
return result
}
手写apply
与call类似,只是第二个参数为数组
手写bind
这里写的比较简单,缺少一些参数判断
Function.prototype.bind = function (thisArgs, ...preparams){
const bindFunc = this
return function(...params){
params = params.concat(preparams)
return bindFunc.apply(thisArgs, params)
}
}
四、for in 与 for of
MDN:
for...of语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
五、如何判断一个空对象
空对象:没有自身的属性
- for in 以任意顺序遍历非Symbol的可枚举属性,包括继承的可枚举属性
缺点:只能检测可枚举属性
function isEmpty(obj){
// 1. 判断是否有 Symbol属性
if( Object.getOwnPropertySymbols(obj).length !== 0){
return false
}
// 2. 判断可枚举属性
for( let key in obj){
if( obj.hasOwnProperty(key) ){
return false
}
}
return true
}
- Object.keys 返回一个自身非Symbol可枚举属性的数组
缺点:只能检测可枚举属性
function isEmpty(obj) {
if (Object.getOwnPropertySymbols(obj).length !== 0) {
return false
}
let arr = Object.keys(obj)
return arr.length > 0 ? false : true
}
- Object.getOwnPropertyNames 返回一个所有自身属性的属性名 (包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
function isEmpty(obj) {
if (Object.getOwnPropertySymbols(obj).length !== 0) {
return false
}
let arr = Object.getOwnPropertyNames(obj)
return arr.length > 0 ? false : true
}
- Reflect.ownKeys 相当于 Object.getOwnPropertyNames(obj).concat( Object.getOwnPropertySymbols(obj) )
function isEmpty(obj) {
let arr = Reflect.ownKeys(obj)
return arr.length > 0 ? false : true
}
六、浅拷贝与深拷贝
浅拷贝
对象浅拷贝:
Object.assign() 方法将所有可枚举(Object.propertyIsEnumerable() 返回 true)和自有(Object.hasOwnProperty() 返回 true)属性从一个或多个源对象复制到目标对象
Object.assign({}, target)
数组浅拷贝:
Array.from(target)
Array.prototype.concat([])
Array.prototype.slice()
展开运算符
{...target}
[...target]
通用浅拷贝
要求实现一个对象参数的浅拷贝并返回拷贝之后的新对象。
注意:
- 参数可能包含函数、正则、日期、ES6新对象
const _shallowClone = target => {
// 如果target为null或undefined
if( target == null){
return target
}
if( target instanceof Function){
return function(){
target.call(this)
}
}
if( target instanceof Date ){
return new Date(target)
}
if( target instanceof RegExp ){
return new RegExp(target)
}
//基本类型
if( typeof target !== 'object'){
return target
}
let clone = new target.constructor()
Reflect.ownKeys(target).forEach( key=>{
clone[key] = target[key]
} )
return clone
}
这里有个小缺陷:无法正确的拷贝 不可枚举 的属性,可以用Object.defineProperty
深拷贝
请补全JavaScript代码,要求实现对象参数的深拷贝并返回拷贝之后的新对象。
注意:
- 需要考虑函数、正则、日期、ES6新对象
- 需要考虑循环引用问题
const _completeDeepClone = (target, map = new WeakMap()) => {
//如果为null或undefined
if (target == null) {
return target
}
//如果为函数则返回一个新函数
if (target instanceof Function) {
return function () {
target.call(this, ...arguments)
}
}
// 返回新正则
if (target instanceof RegExp) {
return new RegExp(target)
}
// 返回新日期
if (target instanceof Date) {
return new Date(target)
}
// 返回原始值
if (typeof target !== 'object') {
return target
}
//解决循环引用的问题
if (map.has(target)) {
return target
} else {
map.set(target, 1)
}
let clone = new target.constructor()
Reflect.ownKeys(target).forEach(key => {
clone[key] = _completeDeepClone(target[key], map)
})
return clone
}
七、var | let | const的区别
1.变量提升
- var存在变量提升
- let | const 不存在变量提升
2.暂时性死区
- var不存在暂时性死区
- let | const 存在暂时性死区
3.重复声明
- var可以重复声明,后声明的会覆盖先声明的
- let | const 不允许重复声明,会报错
4. 块级作用域
- var不存在块级作用域,但有函数作用域
- let | const 存在块级作用域
5.修改声明的变量
- var | let 可以修改声明的变量
- const不能修改声明的变量
6.使用
能使用const尽量使用const,绝大多数使用let,避免使用var
八、什么是闭包
MDN:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建
使用场景
- 创建私有变量
- 延长变量的生命周期
- 函数柯里化
- 装饰器
- 等
九、原型链
十、继承
1. 原型链继承
缺点:子类对象共用同一个原型对象,继承的属性是共享的
function Parent(){
this.name = 'parent1'
this.play = [1,2,3]
}
function Child(){
this.type = 'child2'
}
Child.prototype = new Parent()
let child = new Child()
console.log(child)
2. 构造函数继承(假继承)
call调用父类函数
缺点:只能继承父类的实例属性和方法,没有原型链继承
function Parent(){
this.name = 'parent1'
this.play = [1,2,3]
}
Parent.prototype.getName = function(){
return this.name
}
function Child(){
Parent.call(this)
this.type = 'child'
}
let child = new Child()
console.log(child)
console.log(child.getName())
3. 组合继承(原型链 + 构造函数call)
结合了 原型链 和 构造函数call 的优点
但是 Parent函数执行了两次,导致子类属性和父类属性重复了,正确的做法是:因为子类已经有了父类的属性,所以子类的原型指向父类的原型即可,不需要指向父类对象
function Parent(){
this.name = 'parent'
this.play = [1,2,3]
}
Parent.prototype.getName = function(){
return this.name
}
function Child(){
Parent.call(this)
this.type = 'child'
}
Child.prototype = new Parent() // 这是不好的
//Child.prototype = Object.create(Parent.prototype) // nice,寄生组合式继承就是这样写的
Child.prototype.constructor = Child
let child = new Child()
console.log(child)
4. 原型式继承
缺点:同原型链继承,子类实例对象共用同一个原型对象,属性是共享的
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
}
};
let child1 = Object.create(parent)
child1.name = 'child1'
child1.friends.push('tom')
let child2 = Object.create(parent)
child2.name = 'child2'
child2.friends.push('jack')
console.log(child1)
console.log(child2)
console.log(child2.getName())
5. 寄生组合式继承
几种继承方式的最优解,类似于extends的实现
function Parent(){
this.name = 'parent'
this.play = [1,2,3]
}
Parent.prototype.getName = function(){
return this.name
}
function Child(){
Parent.call(this)
this.type = 'child'
}
function clone(Parent,Child){
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
// 寄生组合式继承与 组合式继承的唯一区别就是子类原型指向的不同
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
}
clone(Parent, Child)
let child = new Child()
console.log(child)
总结
十一、new操作符
new关键字所作的工作
- 创建一个空对象obj
- 将空对象的__proto__指向构造函数的原型对象
- 将构造函数中的this绑定为空对象,运行构造函数
- 根据返回值的类型,忽略原始值,返回对象值
function _new(Func, ...args){
//1. 创建一个空对象obj
let obj = {}
//2. 将空对象的__proto__指向构造函数的原型对象
obj.__proto__ = Func.prototype
//3. 将构造函数中的this绑定为空对象,运行构造函数
let result = Func.apply(obj,...args)
//4. 根据返回值的类型作判断
return result instanceof Object? result:obj
}
十二、尾调用与尾递归(chrome、firefox已经不支持尾调用优化)
尾调用优化的先决条件:
- 开启严格模式
- 必须有return
- return 函数调用
特征:
- 在尾部调用的是函数自身
- 可通过优化,使得计算仅占用常量栈空间
实现一下阶乘,如果用普通的递归,如下:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
factorial(50000) //Maximum call stack size exceeded
如果n等于5,这个方法要执行5次,才返回最终的计算表达式,这样每次都要保存这个方法,就容易造成栈溢出,复杂度为O(n)
如果我们使用尾递归,则如下:
function factorial(n, total=1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
factorial(50000) //不会爆栈
可以看到,每一次返回的就是一个新的函数,不带上一个函数的参数,也就不需要储存上一个函数了。尾递归只需要保存一个调用栈,复杂度 O(1)
十三、垃圾回收机制GC
可达性:一个对象可以通过某种方式访问,则为可达值
- 可达值会存在与内存中,不被回收
- 不可达值会被回收
有哪些可达值
-
这里列出固有的可达值的基本集合,这些值明显不能被释放。
比方说:
- 当前执行的函数,它的局部变量和参数。
- 当前嵌套调用链上的其他函数、它们的局部变量和参数。
- 全局变量。
- (还有一些内部的)
这些值被称作 根(roots) 。
2.如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的。
标记清除算法(mark-and-sweep)
JS引擎的主要垃圾回收算法是标记清除算法
定期执行以下“垃圾回收”步骤: 定期执行以下“垃圾回收”步骤:
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不会对代码执行引入任何延迟。
一些优化建议:
- 分代收集(Generational collection) —— 对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。
- 增量收集(Incremental collection) —— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。
- 闲时收集(Idle-time collection) —— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
引用计数法
早期垃圾回收算法,目前已经不被使用
语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放
如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏
缺点:存在循环引用,导致无法回收
十四、内存泄漏
一个对象的生命到期了,但被另外的对象所引用,导致垃圾回收机制无法回收,产生了内存泄漏
哪些情况会引起内存泄漏
1.意外的全局变量
当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了
2.遗忘的定时器
setTimeout、setInterval由浏览器的定时器模块维护它的生命周期,所以当页面使用了定时器,而当页面销毁时,没有手动的清除定时器,那么这些定时器就是活的,如果定时器的回调函数引用了页面的对象,就会造成内存泄漏,多次打开和关闭页面,内存泄漏会愈加严重
3.使用不当的闭包
闭包:函数与其所在词法环境的组合称为闭包
当闭包返回一个函数,而返回函数引用了这个函数的变量,就会导致函数的词法环境不会被回收。
4.遗漏的DOM元素
当DOM元素从DOM树上卸载时,它的生命周期就结束了,但是如果JS中引用了此DOM,那么该DOM的生命周期就由JS代码和DOM树共同决定了,记得移出时,两个地方都要清理它
5.网络回调
比如在一个网络回调中,该回调函数引用了页面的对象,当页面销毁时,应该注销网络回调,以免页面部分内容无法被回收
十五、事件循环
浏览器与主引擎
我们浏览器是多线程,JS引擎是异步单线程
1. GUI渲染线程
2. JS引擎线程(web worker)
3. 浏览器事件线程(事件)
4. 定时器管理线程
5. http异步线程
6. EventLoop(事件循环)线程
宏任务与微任务
宏任务:
- 主线程代码(script里的代码)
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O流
- UI Render(页面渲染)
- ajax请求
微任务:
- process.nextTick
- Promise
- Async/Await
- MutationObserver(h5新特性)
事件循环算法
- 从宏任务队列(例如'script')中出队(dequeue)并执行最早的任务
- 执行所以微任务
- 当微任务队列非空时:
- 出队并执行最早的微任务
- 如果有变更,则将变更渲染出来。
- 如果宏任务队列为空,则休眠直到出现宏任务
- 转到步骤1
安排一个新的宏任务
- 使用零延迟的
setTimeout(f)
安排一个新的微任务
- 使用
queueMicrotask(f) - promise处理程序也会通过微任务队列
对宏任务与微任务的理解
顾名思义:宏任务涉及的范围更广,微任务涉及的范围小
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染、或进行其他任何操作。
微任务会再执行任何其他事件处理,如渲染,或执行任何其他微任务之前完成。
这很重要,因为它确保了微任务之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)
在每个宏任务之间,浏览器都会检查是否需要重新渲染页面,以便用户实时看到最新的内容,利用这一点,宏任务可被用于将繁重的计算任务拆分成多个部分,以便浏览器能够对用户事件做出反应,并在任务的各部分之间显示任务进度
在微任务之间没有UI或网络事件的处理:它们一个立即接一个的执行。
所以,我们可以使用queueMicrotask(f)来保持环境状态一致的情况下,异步的执行一个函数