【学习记录】JavaScript

118 阅读17分钟

一、数据类型检测

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()

  • 作用:能检测所以数据类型

image.png

5.自定义类型检测函数

实现一个万能的检测函数依靠typeofObject.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
            }
        }

二、数据类型及转换

原始值类型:

  1. number 数字
  2. string 字符串
  3. boolean 布尔
  4. null 空对象指针
  5. undefined 未定义
  6. symbol 唯一值
  7. bigint 大数

对象类型:

  1. 标准普通对象:object
  2. 标准特殊对象:Array、RegExp、Date、Math、Error
  3. 非标准特殊对象:Number、String、Boolean
  4. 可调用\执行对象: Function

1. 其他类型 ---> Number

Number(val)

  • 一般用于浏览器的隐式转换中
  1. 数学运算
  2. isNaN检测
  3. ==

规则:

  1. 字符串转变为数字,空字符串变为0,如果出现任何非有效数字字符的,都变为NaN
  2. true-->1, false-->0
  3. null-->0, undefined-->NaN
  4. Symbol无法转为数字
  5. BigInt去除'n' ,超过安全数的,会按科学计数法
  6. 对象转为数字: 先调用Symbol.toPrimitive这个方法,如果不存在这个方法,继续调用valueOf获取原始值,如果返回的不是原始值,再调用toString把其变为字符串,最后再把字符串基于Number方法转化为数字

parseInt(val,radix)

  • 一般用于显示转换

规则:

val值一定要是字符串,如果不是则先转换为字符串;然后从字符串的左边第一个字符开始找,把找到的有效数字最后转换为数字【一个都没找到就是NaN】,遇到一个非有效数字的字符,则停止查找;parseFloat可以多识别一个小数点

image.png

2. 其他类型---> String

3. 其他类型---> Boolean

  • 除了 0/NaN/空字符串/null/undefined转换为false,其他都是true

4. ==比较时候的相互转换规则

  1. 对象 == 字符串, 先将对象转换为字符串【Symbol.toPrimitive--> valueOf --> toString】,再比较
  2. null == undefined --> true, null/undefined和其他任何值都不相等
  3. 对象 == 对象,比较堆内存地址值
  4. NaN !== NaN , 但是可以用 Object.is(NaN, NaN)-->true
  5. 除了以上情况,只要两边类型不一致,都转换为数字,然后再进行比较

三、this的五种情况

  1. 方法调用,谁调用方法,this就指向谁
  2. 直接调用,非严格模式浏览器中this指向window,node中this指向globl,严格模式下this指向undefined
  3. new调用,this指向实例对象
  4. 事件绑定,this指向绑定的DOM对象
  5. 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...in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

五、如何判断一个空对象

空对象:没有自身的属性

  1. 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
        }
  1. 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
        }
  1. 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
        }
  1. 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]

通用浅拷贝

要求实现一个对象参数的浅拷贝并返回拷贝之后的新对象。
注意:

  1. 参数可能包含函数、正则、日期、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代码,要求实现对象参数的深拷贝并返回拷贝之后的新对象。
注意:

  1. 需要考虑函数、正则、日期、ES6新对象
  2. 需要考虑循环引用问题
 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 中,闭包会随着函数的创建而被同时创建

使用场景

  • 创建私有变量
  • 延长变量的生命周期
  • 函数柯里化
  • 装饰器

九、原型链

image.png

十、继承

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函数执行了两次,导致子类属性和父类属性重复了,正确的做法是:因为子类已经有了父类的属性,所以子类的原型指向父类的原型即可,不需要指向父类对象

image.png

        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) 

总结

image.png

十一、new操作符

new关键字所作的工作

  1. 创建一个空对象obj
  2. 将空对象的__proto__指向构造函数的原型对象
  3. 将构造函数中的this绑定为空对象,运行构造函数
  4. 根据返回值的类型,忽略原始值,返回对象值
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已经不支持尾调用优化)

尾调用优化的先决条件:

  1. 开启严格模式
  2. 必须有return
  3. 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

可达性:一个对象可以通过某种方式访问,则为可达值

  • 可达值会存在与内存中,不被回收
  • 不可达值会被回收

有哪些可达值

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。

    比方说:

    • 当前执行的函数,它的局部变量和参数。
    • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
    • 全局变量。
    • (还有一些内部的)

    这些值被称作 根(roots)

2.如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的。

标记清除算法(mark-and-sweep)

JS引擎的主要垃圾回收算法是标记清除算法

定期执行以下“垃圾回收”步骤: 定期执行以下“垃圾回收”步骤:

  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 然后它遍历并“标记”来自它们的所有引用。
  • 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  • ……如此操作,直到所有可达的(从根部)引用都被访问到。
  • 没有被标记的对象都会被删除。

这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不会对代码执行引入任何延迟。

一些优化建议:

  • 分代收集(Generational collection) —— 对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。
  • 增量收集(Incremental collection) —— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。
  • 闲时收集(Idle-time collection) —— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

引用计数法

早期垃圾回收算法,目前已经不被使用

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

缺点:存在循环引用,导致无法回收

十四、内存泄漏

一个对象的生命到期了,但被另外的对象所引用,导致垃圾回收机制无法回收,产生了内存泄漏

哪些情况会引起内存泄漏

1.意外的全局变量

当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了

2.遗忘的定时器

setTimeoutsetInterval由浏览器的定时器模块维护它的生命周期,所以当页面使用了定时器,而当页面销毁时,没有手动的清除定时器,那么这些定时器就是活的,如果定时器的回调函数引用了页面的对象,就会造成内存泄漏,多次打开和关闭页面,内存泄漏会愈加严重

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新特性)

事件循环算法

  1. 从宏任务队列(例如'script')中出队(dequeue)并执行最早的任务
  2. 执行所以微任务
    • 当微任务队列非空时:
    • 出队并执行最早的微任务
  3. 如果有变更,则将变更渲染出来。
  4. 如果宏任务队列为空,则休眠直到出现宏任务
  5. 转到步骤1

安排一个新的宏任务

  • 使用零延迟的setTimeout(f)

安排一个新的微任务

  • 使用queueMicrotask(f)
  • promise处理程序也会通过微任务队列

对宏任务与微任务的理解

顾名思义:宏任务涉及的范围更广,微任务涉及的范围小

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染、或进行其他任何操作。

微任务会再执行任何其他事件处理,如渲染,或执行任何其他微任务之前完成。

这很重要,因为它确保了微任务之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)

在每个宏任务之间,浏览器都会检查是否需要重新渲染页面,以便用户实时看到最新的内容,利用这一点,宏任务可被用于将繁重的计算任务拆分成多个部分,以便浏览器能够对用户事件做出反应,并在任务的各部分之间显示任务进度

在微任务之间没有UI或网络事件的处理:它们一个立即接一个的执行。

所以,我们可以使用queueMicrotask(f)来保持环境状态一致的情况下,异步的执行一个函数