• 前端面试题之Javascript篇


    一、JavaScript基础

    1、数组有哪些方法

    添加/删除元素

    • push() 向尾部添加元素
    • pop() 从尾部提取一个元素
    • shift() 从首端提取元素
    • unshift() 从首端添加元素
    • splice(start, deleteCount, item1...itemN) start表示开始计算的索引,deleteCount表示从start开始计算的元素的个数,item1...itemN 从start开始要加入到数组的元素。
    • slice(start, end) 返回一个新的对象,该对象由startend决定的原数组的浅拷贝
    • arr.concat(item) 向arr中添加item

    搜索元素

    • indexOf/lastIndexOf(item, start) 从索引start开始(不填从开头或者末尾开始)搜索item,搜索到则返回item的下标,否则返回-1
    • includes(item) 如果数组有item,则返回true,否则返回false
    • find/filter(func) 通过func返回符合条件的第一个值/所有值
    • findIndexfind雷类似,但返回的是索引而不是值

    操作数组

    • forEach(func) 遍历数组,对每个元素都调用func
    • map(func) 表里数组,每个元素经过func处理之后,返回新的数组
    • sort() 对数组进行排序,然后返回
    • reverse() 反转数组,然后返回
    • split/join 将字符串转换为数组并返回
    • reduce/reduceRight(func, initial) 方法对数组中的每个元素按序执行一个提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。reduceRight方向相反(从右到左)。

    还有一些其他的方法参见 MDN

    2、什么是DOM和BOM

    • DOM是文档对象模型,指的是把文档当做一个对象,这个对象定义了网页内容的方法和接口
    • BOM是浏览器对象模型,它指的是把浏览器当做一个对象来对待。这个对象包含了浏览器交互的方法和接口,BOM的核心是window,window既能被js访问,又是一个Global(全局)对象,window对象包含有location对象,navigator对象等等

    3、对AJAX的理解,实现一个AJAX请求

    AJAX 指的是JavaScript的异步通讯,是通过XMLHttpRequest()实现网络通讯。

    const url = "/server"
    let xml = new XMLHttpRequest()
    
    // 监听状态码
    xml.onreadystatechange = function() {
        if(this.readyState !== 4) return;
    
        // 请求成功
        if(this.status === 200) {
            console.log("请求成功了")
            console.log(this.response)
        } else {
            console.log("请求失败")
            console.log(this.status,this.statusText)  
        }
    }
    
    // 监听请求失败
    xml.onerror = function() {
        console.log(this.statusText)
    }
    
    // 创建Http请求
    xml.open("GET", url, true)
    
    // 设置返回的的类型
    xml.responseType = "json"
    // 设置请求头
    xml.setRequestHeader("Accept", "application/json")
    
    // 发送请求
    xml.send(null)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    4、new 操作符的实现原理

    要搞清楚这个问题,首先要知道 实际中 new 到底做了什么,来看个例子:

    第一种情况

        function Animal(name) {
            this.name = name
            this.run = function(){}
        }
        let cat = new Animal('猫咪')
        console.log(cat)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    运行结果如下:

    可以得出以下结论:

    1. cat 获取到了Animal的所有属性和方法
    2. cat 的构造函数指向Animal

    第二种情况

        function Animal(name) {
            this.name = name
            this.run = function(){}
    
            return {type: "all"} // or return function() {...}
        }
        let cat = new Animal('猫咪')
        console.log(cat)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果如下:

    可以得出结论:

    1. 当Animal有返回Object类型的时候,cat的值是返回的object

    那么总结一下new的过程需要干什么:
    (1).让cat 获取到Animal的所有属性和方法,并绑定this到cat上
    (2).将cat 的原型指向Animal
    (3).判断是否Animal上有object类型的返回值

    代码实现如下:

    function myNew() {
        // 创建一个空对象
        const newObj = {}
        // 从参数中获取构造函数(第一个参数)
        // arguments是类数组,call能让arguments可以用shift的方法
        // 此时arguments已经被取走第一个参数
        console.log(arguments)
        const constructor = Array.prototype.shift.call(arguments)
        // 让创建的对象的__proto__指向constructor的原型
        newObj.__proto__ = constructor.prototype
    
        // 执行constructor方法,并绑定this到newObj上
        const result = constructor.apply(newObj, arguments)
    
        // 判断返回值
        return (result instanceof Object) ? result : newObj
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5、for…in 和 for…of的区别

    for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构,并返回各项的值,与ES3中的for…in的区别如下:

    • for…of 遍历获取到到的是对象的键值;for…in 获取到的是键名
    • for…in 会遍历对象的整个原型链,性能非常差不推荐。for…of只会遍历当前对象,不会遍历原型链
    • 对于数组的遍历,for…in 会遍历数组中可枚举的属性,包括原型链,而for…of只会遍历当前数据的下标对应的值

    总结 for…in 循环主要是为了遍历对象而生,不适合遍历遍历数组。for…of适合遍历数组、类数组、字符串、Set、Map以及Generator对象。

    for…of 遍历类数组,可以结合Array.from来转换成数组:

    const obj = {
        0: 11,
        1: 22,
        length:2 
    }
    for(const b of Array.from(obj)) {
        console.log(b)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    6、JavaScript 延时加载的方式有哪些

    1. 使用defer,页面的渲染不会被阻塞,这个属性会让脚本的加载和文档的解析同步解析,但是这个脚本是在文档解析完成后执行。
    2. 使用async,页面的渲染不会被阻塞,这个属性会让脚本的加载和文档的解析同步解析,和defer的区别是async加载完成之后就会立即执行。
    3. 使用动态创建DOM的方式,监听页面加载完成事件,再创建script标签
    4. 在文档的底部引用js文件
    5. 使用setTimeout延时加载js文件

    二、数据类型

    1、判断数据类型的方法有哪些

    (1)typeof

        console.log(typeof true)        // boolean
        console.log(typeof 2)           // number
        console.log(typeof "hello")     // string
        console.log(typeof undefined)   // undefined
        console.log(typeof null)        // object
        console.log(typeof {})          // object
        console.log(typeof [])          // object
        console.log(typeof Symbol)      // function
        console.log(typeof BigInt)      // function
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中null、数组、对象都判断为object

    (2)instanceof
    instanceof可以正确的判断对象类型,其内部的运行机制是在其原型链中是否能找到该类型的原型。

    console.log(1 instanceof Number)                // false
    console.log(true instanceof Boolean)            // false
    console.log('hello' instanceof String)          // false
    console.log([1,2,3] instanceof Array)           // true
    console.log({name:"tom"} instanceof Object)     // true
    console.log(function(){} instanceof Function)   // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可见instanceof不能判断 number、string、boolean基础类型,只能用来判断引用类型

    (3)constructor

    console.log((1).constructor)                        // true
    console.log((1).constructor === Number)             // true
    console.log(('hello').constructor === String)       // true
    console.log((true).constructor === Boolean)         // true
    console.log(([1,2,3]).constructor === Array)        // true
    console.log(({}).constructor === Object)            // true
    console.log((function(){}).constructor === Function)// true
    console.log((()=>{}).constructor === Function)      // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    constructor可以用来判断数据结构,也可以通过constructor来访问它的构造函数。需要注意的是如果改变了它的constructor的话,判断会不准确

    (4)Object.prototype.toString.cell()

    var a = Object.prototype.toString
    console.log(a.call(1))              // [object Number]
    console.log(a.call(true))           // [object Boolean]
    console.log(a.call('hello'))        // [object String]
    console.log(a.call([]))             // [object Array]
    console.log(a.call({}))             // [object Object]
    console.log(a.call(function(){}))   // [object Function]
    console.log(a.call(undefined))      // [object Undefined]
    console.log(a.call(null))           // [object Null]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可见该方法能正确判断常见的类型

    2、为什么typeof null是object

    因为在第一个JavaScript第一个版本中,所有值都存储在一个32位的单元中,用低位的1或者3个bit来表示类型。其他区域存储真实的数据,例如:

    000:object		// 当前存储的是一个对象
      1:int		// 当前存储的数据是一个31位的有符号整数
    010:double		// 当前存储的数据指向一个双精度的浮点数
    100:string		// 当前存储的数据指向一个字符串
    110:boolean	// 当前存储的的数据是布尔值
    
    • 1
    • 2
    • 3
    • 4
    • 5

    null的值是机器码NULL指针(null指针的值全是0),和object的类型标签一样全是000,所以typeof null被识别为object

    3、null和undefined的区别

    首先undefined和null都是基本数据类型,undefined代表的含义是未定义,null代表的含义是空值,一般变量声明了但还未定义的时候会返回undefined

    如何得到一个安全的undefined?
    可通过void 0 来得到一个安全的undefined,因为在JavaScript中undefined不是一个保留字,就意味着他可以作为一个变量来使用,会影响对undefined的判断,例如:

    function fn() {
        var undefined = 1
        console.log(undefined)
    }
    fn() // 1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4、instanceof的原理

    instanceof 用来判断构造函数的prototype属性是否出现在原型链上的任一位置

    function myInstanceof(left, right) {
        // 获取原型
        let proto = Object.getPrototypeOf(left)
        // 获取构造函数的prototype
        const prototype = right.prototype
        // 循环遍历
        while(true) {
            if(!proto) return false;
            if(proto === prototype) return true;
            // 沿着原型链往上找
            proto = Object.getPrototypeOf(proto)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    三、原型和原型链

    1、对原型和原型链的理解

    在这里插入图片描述

    四、执行上下文/作用域链 /闭包

    1、对闭包的理解

    闭包是指有权访问另一个函数作用域的函数,最常用创建闭包的方式就是在一个函数中创建另一个函数,创建的函数可以访问到当前函数的局部变量。

    闭包的两个常用的用途

    • 闭包的第一个用途是使在函数外部能访问到函数内部的变量,通过使用闭包函数,从而在外部访问到函数内部的变量,可以用这种方法来创建私有变量
    • 闭包的第二个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包中保存了这个变量对象的引用,所以这个变量对象不会被回收

    5、this、call、apply、bind

    1、对this的理解

    this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this的指向可以用以下四种调用模式来判断

    • 函数调用模式,当一个函数直接作为函数来调用时,this指向全局
    • 方法调用模式,当函数作为一个对象的方法来进行调用的时候,this指向这个对象
    • 构造器调用模式,如果一个函数用new来调用时,函数执行前会新创建一个对象,this指向这个新创建的对象
    • call、apply和bind调用模式,这三个方法显式的指定调用的this指向,this指向参数中的this

    这四种方式的优先级:构造器调用模式 > call、apply和bind调用模式 > 方法调用模式 > 函数调用模式

    2、call() 和 apply() 的区别

    它们的作用一模一样,区别在于传入的参数形式不同

    这个函数几乎与 apply() 相同,只是函数的参数以列表的形式逐个传递给 call(),而在 apply() 中它们被组合在一个对象中,通常是一个数组——例如,func.call(this, “eat”, “bananas”) 与 func.apply(this, [“eat”, “bananas”])。

    3、实现call、apply和bind函数

    call函数
    call 的语法为call(this, arg1...argN)

    • this:表示指向的this,如果函数不在严格模式下,null 和 undefined 将被替换为全局对象,并且原始值将被转换为对象。
    • arg1...argN:表示函数参数
        Function.prototype.myCall = function(context) {
            // 判断调用对象
            if(typeof this !== 'function') {
                console.log("type error")
            }
            // 获取参数除了第一个之后的参数
            let args = [...arguments].slice(1)
            // 判断 context 是否传入, 未传入则设置为window
            context = context || window
            // 函数调用的结果
            let result = null
            // 将调用函数设为对象的方法
            context.fn = this // this是原函数
            // 调用原函数(相当于使用了对象方法调用的方式,this绑定在对象上)
            result = context.fn(...args)
            // 将属性删除
            delete context.fn
            return result
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    apply函数
    apply 的语法为call(this, [arg1...argN])

    • this:表示指向的this,如果函数不在严格模式下,null 和 undefined 将被替换为全局对象,并且原始值将被转换为对象。
    • [arg1...argN]:表示函数参数,是一个数组
        Function.prototype.myApply = function(context) {
            // 判断调用对象
            if(typeof this !== 'function') {
                console.log("type error")
            }
            // 判断是否存在context
            context = context || window
            // 函数调用的结果
            let result = null
            // 将调用函数设为对象的方法
            context.fn = this // this是原函数
    
            // 判断是否存在参数
            if(arguments[1]){
                result = context.fn(...arguments[1]) // arg1...argN
            } else {
                result = context.fn()
            }
            // 将属性删除
            delete context.fn
            return result
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    bind函数
    bind() 方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。

    bind()的语法为bind(this, arg1, arg2, / …, / argN)

    在调用绑定函数时,作为 this 参数传入目标函数 func 的值。如果函数不在严格模式下,null 和 undefined 会被替换为全局对象,并且原始值会被转换为对象。如果使用 new 运算符构造绑定函数,则忽略该值。

        Function.prototype.myBind = function(context) {
            // 判断调用对象
            if(typeof this !== 'function') {
                console.log("type error")
            }
            // 获取参数
            let args = [...arguments].slice(1)
            let fn = this
    
            return function Fn() {
                return fn.apply(
                    this instanceof Fn ? this : context,
                    args.concat(...arguments)
                )
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5、异步编程

    1、异步编程的实现方式

    • 回调函数 回调函数的缺点是多个回调函数嵌套时会造成回调函数地域,不利于代码维护。
    • Promise 使用Promise的方式可以将嵌套的回调函数作为链式调用,用这种方式有时会造成多个then的链式调用,有可能会造成代码的语义不够明确
    • Generator她可以在函数执行的过程中,将函数的执行权移交出去,在函数的外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完成,再将函数的执行权转移回来
    • async函数 async函数是generator和Promise实现的自动执行的语法糖

    2、对Promise的理解

    Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,能够避免回调地域的问题。

    (1)Promise有三种状态

    • pending:初始状态(进行中)
    • fulfilled:操作成功
    • rejected:操作失败

    (2)Promise的实例有两个过程

    • pending -> fulfilled:resolved(已完成)
    • pending -> rejected:rejected(已拒绝)

    注意:一旦从进行状态变成其他状态就永远不能改变状态了

    3、Promise的基本用法

    Promise 构造函数接收一个函数作为参数,该函数的两个参数分别是resolvereject

        const promise = new Promise((resolve, reject) => {
            if(true) {
                resolve(1)
            } else {
                reject(2)
            }
        })
    
        promise.then((e) => {
            console.log('success:' + e)
        }).catch((err) => {
            console.log('err:' + err)
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Promise 的方法
    Promise有五个常用的方法:then()、catch()、all()、race()、finally()

    1. then()
      then 方法接收两个回调函数作为参数。第一个回调函数是Promise状态变成resolved的时候调用,第二个回调函数的是Promise对象的状态变成rejected时调用。其中第二个参数可以省略。then返回的是一个新的Promise实例(不是原来的那个Promise实例),所以then后面还可以接着链式调用then方法。
        const promise = new Promise((resolve, reject) => {
            resolve(1)
        })
        promise.then((e) => {
            console.log('success:' + e)
            return new Promise((resolve, reject) => {
                resolve(3)
            })
        }).then((e) => {
            console.log("成功:", e)
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. catch() 方法
      catch的作用有两个。一是Promise的状态变成rejected的时候会进入到catch函数中,二是在Promise执行的过程中如果发生异常,则也会进入到catch中。
        const promise = new Promise((resolve, reject) => {
            // throw("123") 抛出异常
            reject(1)
        })
        promise.then((e) => {
            console.log('success:' + e)
        }).catch((err) => {
            console.log("error:", err)
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. all() 方法
      all 方法可以完成并行任务,它接收一个数组,数组的每一项都是Promise实例,数组中的Promise的状态都变成resolved时,all方法的状态就会变成resolved,否则只要其中一个变成了rejected,那么all的状态也会变成rejected
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 2000)
        })
        const p2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(2)
            }, 1000)
        })
        const p3 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(3)
            }, 3000)
        })
    
        Promise.all([p1,p2,p3]).then((e) => {
            console.log(e)
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    结果是[1,2,3] 可见all方式的结果是按顺序的

    1. race()
      race() 方法和all方法一样的用法,区别在于,当数组中的Promise有一个执行完成,那么就立即返回第一个执行完成的Promise的值,如果第一个Promise的状态是resolved,那么race的状态也会变成resolved,反之第一个是rejected的话,race也会变成rejected。

    2. finally()
      finally 方法不管Promise的状态如何,都会执行finally方法。

    4、async/await 的理解

    async/await 其实是generator和Promise的语法糖,他能实现的效果都能用then链式来实现,它是用来优化then的。Promise是用来解决回调地狱的问题,而async/await 被发明用来进一步优化Promise。

    await 表达式的运行结果取决于它等的是什么

    • 如果它等的不是一个Promise对象,那么await表达式的运算结果就是它的结果
    • 如果等到的是一个Promise 对象,await就忙起来了,他会阻塞后面代码的执行,等Promise的resolve,然后得到resolve的值,作为await表达式的运算结果

    async / await 可以像同步代码一样运行

    async function doIt() {
    	const time1 = 300;
    	const time2 = await promise1(time1);
    	const time3 = await promise2(time2);
    	const result = await promise3(time3);
    	console.log("result is " + result)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    解决了Promise代码中的then链式调用带来额外的阅读负担,上例中的代码如果用then来调用的话,会变成下面这样子:

    async function doIt() {
    	const time1 = 300;
    	promise1(time1)
    			.then(time2 => step2(time2))
    			.then(time3 => step2(time3))
    			.then(result => {
    				console.log("result is " + result)				
    			})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    await 只能得到resolve的结果, 异常的结果可以使用try/catch语法捕捉异常

    六、垃圾回收机制

    1、垃圾回收的方式

    • 标记清除
      标记清除是浏览器最常用的垃圾回收方式。
    • 引用计数

    减少垃圾回收
    虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂的时候,垃圾回收带来的代价也是比较大的,所以应该尽量减少垃圾回收

    • 对数组进行优化 在清空一个数组时,最简单的方法就是给其赋值为[],但是也会创建一个空对象,可以将数组的长度设为0,以此来达到清空数组的目的
    • 对object优化对象尽量复用,对于不再使用的对象,就将其设置为null,尽快回收
    • 对函数进行优化 在循环中的函数表达式,如果能复用,尽量放在函数的外部

    2、那些情况会导致内存泄漏

    • 以外的全局变量 由于使用未声明的变量,而意外创建了一个全局变量,这个变量会一直留在内存中无法被回收
    • 被遗忘的计时器或回调函数 设置了setInterval 定时器而忘记取消它,就会造成内存泄漏。还有就是循环函数有引用外部的变量,那么这个变量也不会被回收
    • 脱离DOM的引用 获取DOM元素的引用,而后面把这个元素删除,由于保留了这个DOM的引用,则这个DOM无法被回收
    • 闭包 不合理的使用闭包,从而导致某些变量一直留在内存中
  • 相关阅读:
    设计模式 - 单例模式理解及相关问题解决方法
    分布式机器学习:异步SGD和Hogwild!算法(Pytorch)
    JoySSL证书买二送一买三送二特别活动
    LLM应用实战:当KBQA集成LLM(二)
    OpenP2P实现内网穿透远程办公
    1688关键字搜索商品
    如何批量修改文件名按顺序命名
    史上最全 Zuul网关鉴权 范文
    【前端设计模式】之迭代器模式
    浏览器原理思维导图
  • 原文地址:https://blog.csdn.net/qq_43706089/article/details/134239482