ES6 常见面试题

let、const、var 区别

{% tabs ES6 %}

{% note info orange %} 1.存在块级作用域 2.不能在相同作用域下重复声明 3.声明的值可以修改,可以先声明再进行赋值 {% endnote %}

   let test;
   test = "aa"

{% note info orange %} 1.声明了必须赋值,否则会报错 2.定义的值不能被修改 {% endnote %}


    // const test;  //错误,没有赋值
    const test = "Test";
    // test = "Test2" 错误,不能修改它的值

{% note info orange %} 1.变量提升(声明提升) 2.变量覆盖 3.没有块级作用域 {% endnote %}

    // 1.变量提升
    // 出现的问题:在控制台只会输出number的值为 undefined
    // 原因:用var声明的变量会作用于全局变量中
    console.log(number);
    var numbe  = 1;

    // 2.变量覆盖
    // 出现的问题:最后一个值会覆盖上一个相同的值
    // 原因:用var声明相同的两个变量,最后一个会覆盖上面相同的变量
    var num1 = 2;
    var num1 = 3;
    console.log(num2) // 输出num1的值为3


    // 3.没有块级作用域
    // 出现的问题:超出作用域
    // 原因:var 声明的变量是全局变量
    function fn(){

        for(var i= 0;i<=3;i++>){
            console.log(i) // 输出0,1,2,3
        }
        console.log(i) // 输出3 
    }

{% endtabs %}

面试题

 //1.使用ES6将两个值进行互换
  let a = 10;
  let b = 20;
  [a,b] = [b,a]

  //2.使用ES6快速去重
  let arr = [14,12,14,25,67,77,77]

  let newArr = [...new Set(arr)]

    //3.promise的构造函数
    // 构造函数是同步执行的
    const promise = new Promise((resolve,reject) => {
        console.log(1)
        resolve()
        console.log(2)
    })
    // 异步执行
    promise.then(res => {
        console.log(3)
    })
    // 同步执行
    console.log(4)
    // 控制台会输出:1,2,4,3

js常见面试题

深拷贝和浅拷贝

{% note info orange %} 浅拷贝:简单来说,浅拷贝只是拷贝了对象的引用``,而没有拷贝对象本身`。 {% endnote %} 案例演示:

    var obj = {
        a:11,
        b:12,
        c:13
    }
    var obj2 = obj; // 浅拷贝
    obj2.a = 14
    // 这里 obj 和 obj2 出的的a的值都会被输出为14,因为浅拷贝拷贝的是同一个对象的地址的引用,所以obj2改变了obj也会被改变
    console.log(obj)
    console.log(obj2)

{% note info orange %} 深拷贝:简单来说,深拷贝是创建了一个与原始对象完全独立的对象。 {% endnote %}

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.a = 3;
obj2.b.c = 4;

console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 3, b: { c: 4 } }

需要注意的是,使用 JSON.stringify() 和 JSON.parse() 进行深拷贝时,原始对象中的函数、正则表达式、Map、Set 等特殊类型数据会丢失。此外,递归方法也可能会存在性能问题,因此需要谨慎使用。

防抖与节流

{% note info orange %} 防抖和节流本质上都是用来减少函数执行的频率,以达到优化项目性能或者实现特定功能的效果 {% endnote %} {% label 防抖 orange %} 定义:在n秒后执行函数,若n秒内重复触发只会执行一次 代码实现:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<body>
    <input type="text" placeholder="text">
</body>
<script>
   // 防抖 ==> 将多次操作变成一次
    const inputEle = document.querySelector("input");
    // 防抖函数封装
    // fn : 执行函数
    // wait: 事件(秒)
    const antiShakeFun = (fn,wait) => {
        let timeOut = null;
        return arge => {
            // 如果timeOut存在,清除timeOut重新计时
            if(timeOut) clearTimeout(timeOut);
            timeOut = setTimeout(fn,wait)
        }
    }
    const getTargetVale  = (e) => {
        console.log()
    }
    // 监听input的输入事件
    // 没有函数防抖
    // inputEle.addEventListener('input',(e) => {
    //     console.log(e.target.value) 
    // })

    // 函数防抖
    inputEle.addEventListener('input',antiShakeFun(getTargetVale,2000))
</script>
</html>

{% label 节流 orange %} 定义:在n秒内只调用一次函数,若n秒内重复触发只会调用一次 代码实现:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<style>
    #box {
        width: 400px;
        height: 400px;
        background-color: gray;
    }
</style>
<body>
    <div id="box"></div>
</body>
<script>
  // 节流
  const boxEle = document.querySelector("#box");
  const testDemo = () => {
    console.log('aa')
  }
    // 封装节流函数
    const throttleFun = (fn,wait) => {
    let time = null;

    return () => {
        if(!time){
            // 如果在n秒内重复执行,只执行一次fn,并且重新计时
            time = setTimeout(() => {
                fn()
                time = null
            },wait)
        }
    }
  }
//   boxEle.addEventListener("touchmove",() => {
//     console.log("aa") // 没节流之前,当我们鼠标移动时,这里会不断重复的执行
//   })
  boxEle.addEventListener("touchmove",throttleFun(testDemo,2000))
</script>
</html>

原型和原型链

{% tabs 原型与原型链 %}

{% note info simple %} 原型:原型是函数特有的,普通的对象和数组时没有原型的 表示方式:prototype {% endnote %}

    // 原型 ==> prototype ==> 是函数特有的
    // 原型链 ==> _proto_ ==> [[prototype]]
    let obj = {};
    // let arr = [];
    // obj.prototype.a = "aa" // 控制台报错,普通的对象和数组时没有原型的
    console.log(obj)
    function fn(){

    }
    fn.prototype.name = "aa"  // 正确,我们可以给fn的原型上定义一些独有的属性和方法
    console.dir(fn)

{% note info simple %} 原型链:原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数,这样实例和原型之间构造一条原型链。这就是原型链的基本构想 原型链最顶端是null {% endnote %} 原型链主要是用来实现继承的,下面我们通过一个小案例实现一个原型链继承

 // 原型链 ==> _proto_ ==> [[prototype]]
    function Father(){
        this.nature = "张"
    }
    Father.prototype.getFatherValue = function (){
        return this.nature
    }
    function Son(){
        this.name = "三"
    }
    // 儿子继承父亲
    Son.prototype = new Father();
    // 构造son的实例
    let instance = new Son();
    //通过Son实例访问Father的getFatherValue方法
    const nature = instance.getFatherValue()
    console.log(nature + instance.name) // 张三

以上代码我们分别定义了两个类型: FatherSon,我们在Father的原型prototype上定义一个函数(getFatherValue)来获取父亲私有的一个属性nature,然后我们创建Father 的实例(new Father())将它赋值给Son的一个原型上,实现了对Father的继承。这个赋值重写了Son的最初的原型,将其替换为Father的实例。这就意味着Father实例上所有存在的属性和方法也会存在Son的实例中,所有我们通过Son的一个实例就可以调用到Father上面的属性和方法,这样我们就实现了一个继承

{% endtabs %}

实现Ajax请求失败重连

    //实现ajax失败重连,失败3次后终止请求
    function request(){
        return new Promise((resolve,reject)=> {
            setTimeout(()=> {
                let random  = Math.random();
                if(random > 0.4){
                    resolve("连接成功!")
                    console.log("连接成功!")
                }else{
                    reject('连接失败')
                    console.log('连接失败')
                }
            })
        })

    }
    /**
     * 
     * api: 请求的函数
     * times: 请求失败后允许重连的次数
     * delay:延迟时间
     **/
    function resetRequest (api,times,delay){

        return new Promise((resolve,reject) => {

            let inner = async() => {
                try {
                    const result = await api();
                    resolve(result)
                } catch (e) {
                    //如果失败了要进行重连,例如失败了3次,就返回失败结果,不再进行重连
                    if(times-- <=0){
                        reject("重连失败。。。。")
                    }else{
                        // 延迟执行
                        setTimeout(() => {
                            inner();
                            console.log("正在重连。。。",times)
                        }, delay);
                        
                    }
                }
            };
            inner();
        })
    }
    resetRequest(request,10,2000)

this指向拆解

{% note info simple %} 面向对象语言中 this 表示当前对象的一个引用。 但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。

  • 在方法中,this 表示该方法所属的对象。
  • 如果单独使用,this 表示全局对象。
  • 在函数中,this 表示全局对象。
  • 在函数中,在严格模式下,this 是未定义的(undefined)。
  • 在构造函数中,this指向的是被构造的实例
  • 在事件中,this 表示接收事件的元素。
  • 类似 call() 和 apply() 方法可以将 this 引用到任何对象。 {% endnote %}

全局this

js中,this指向全局对象,在浏览器中这个全局对象指的就是window对象,单独使用this或者在函数中使用this表示的都是全局对象

方法中的this

在对象方法中,this指向的是对象的本身

严格模式下的this

严格模式下,this的指向是undefined

事件中的this

事件中的this指向的是接收事件的HTML元素,例如这里指向的就是button元素

构造函数中的this

在构造函数中,this指向的是被构造的实例,例如下面通过new Person()构造了一个叫son的一个实例,内部的this就是指向son的这个实例 {% label 注意: orange %} 在构造函数中 this 其实是经过js内部自动转换来的

  function Person(){
    // 下面是js内部自动帮我们做的
    // var obj = {};
    // obj.__proto__ = Person.__proto__
    // this = obj
    console.log(this)  // 其实指向的 --> son 实例
    this.name = "NickYang"
    this.age = 18
    // 下面是我们模拟js内部做的实现
    //   // 1.创建一个新对象
    // const obj = {}
    // // 2.新对象原型指向构造函数原型对象
    // obj.__proto__ = Func.prototype
    // // 3.将构建函数的this指向新对象
    // let result = Func.apply(obj, args)
    // // 4.根据返回值判断
    // return result instanceof Object ? result : obj
  }

  const son = new Person();
  console.log(son,'son')

new 操作符具体做了什么?

JavaScript 中的 new 操作符用于创建对象的实例。它的工作原理是:

  • 创建一个空对象。
  • 将这个对象的原型指向构造函数的 prototype 属性。
  • 将这个对象作为构造函数的 this 值,并调用构造函数。
  • 如果构造函数返回一个对象,则返回这个对象;否则,返回创建的对象。 下面是一个示例代码,展示了 new 操作符的用法:
// 定义构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 定义一个方法,用于输出人的信息
Person.prototype.sayHello = function() {
    console.log("我叫 " + this.name + ",今年 " + this.age + " 岁。");
}

// 使用 new 操作符创建一个 Person 对象
var p = new Person("John", 30);

// 调用 Person 对象的 sayHello 方法
p.sayHello();

下面我们自己实现一个new的操作符

function create(fn,...args){
    // 1.声明空对象
    var obj = {};
    // 2.将空对象的原型,指向构造函数的原型
    Object.setPrototypeOf(obj,fn.prototype);
    // 3. 将空对象的上下文指向函数的上下文(改变this指向)
    var result = fn.apply(obj,args);
    // 4. 返回对象
    return result instanceof Object ? result:obj
}

function Person (age){

    console.log(age)
}

const p1 = create(Person,18)

call、apply、bind原理解析

jscallapplybind这三个方法都是js提供给我们的可以用来改变this的指向,下面我们通过自己手写的方式来实现自己的callapplybind方法

call方法实现

{% tabs call方法实现 %}

{% note info simple %} call 方法说明:

  • 可以接收多个参数,至少不能少于1个
  • 第一个参数为要改变的this指向,往后是形参
  • 所有的函数都可以调用 {% endnote %}
 // 要求:将getInfo方法内部的this指向person,使得我们可以通过this直接访问到person中的name的值
 function getInfo(age){
    console.log(this,'this'); // 没调用call方法之前,this指向的是window对象,调用了call,this指向person
    console.log(this.name) // 输出Nick
    // return {
    //     name:this.name,
    //     age:age
    // }
 }

 var person = {
    name:"Nick",
    age:18
 }
// 调用call方法,将getInfo中this的指向person
   getInfo.call(person,18);

{% note info simple %} 自定义call实现步骤:

  • 所有的函数都可以调用
  • 可以接收多个参数,至少有一个
  • 接收的参数第一个为this指向的对象 {% endnote %}
 // 1.声明call方法,obj是我们的要改变的this的指向,如果要传多个,在obj后面继续添加即可
  var customCall = function (obj,age,type){
    // 保证obj是一个对象,防止参数传入无效的值出现bug
    obj = obj || window;

    var arr = [];

    //2.获取传入的参数 --> age,type等
    for(let index = 1;index <arguments.length;index++){
        const params = arguments[index];
        arr.push(params)
    }
   //使用Symbol的原因:防止与提供新的 this 值的属性重复
    let symbolFn = Symbol('fn')

    obj[symbolFn] = this;
    let result = obj[symbolFn](...arr);

    delete obj[symbolFn]

    return result

  }
  var person = {
    name:'Nick'
  }

  // 要让所有的函数都能访问必须在方法原型链上定义
  Function.prototype.customCall = customCall;


  function test(){
    console.log(this,'test') // 没调用customCall之前this指向window,调用了之后指向person
    console.log(this.name) // 输出Nick
  }

//   test()

  test.customCall(person,18)

{% endtabs %}

apply方法实现

{% note info simple %} 注:apply方法和call方法实现原理是一样,只是两者区别在于传的参数稍微有点不同,apply方法第二个参数规定是传数组,所以我们只要在原有基础上修改了获取参数的方式即可 {% endnote %}

 // 参数:第一个是我们的要改变的this的指向,第二个是数组
  var customApply = function (obj){
    

    // 保证obj是一个对象,防止参数传入无效的值出现bug
    obj = obj || window;

    
    // 获取第二个参数,该参数是一个数组
    var arr = arguments[1]
    // console.log(arguments)
    // for(let index = 1;index <arguments.length;index++){
    //     const params = arguments[index];
    //     arr.push(params)
    // }
   //使用Symbol的原因:防止与提供新的 this 值的属性重复
    let symbolFn = Symbol('fn')

    obj[symbolFn] = this;
    let result = obj[symbolFn](...arr);

    delete obj[symbolFn]
    return result
  }
  var person = {
    name:'Nick'
  }

  // 要让所有的函数都能访问必须在方法原型链上定义
  Function.prototype.customApply = customApply;


  function test(){
    console.log(this,'test') // 没调用customCall之前this指向window,调用了之后指向person
    console.log(this.name) // 输出Nick
  }

//   test()

  test.customApply(person,['14','12'])

区别:

{% note warning simple %}

  • callapply可以立即执行,bind方法不行,因为bind方法返回的是一个函数,需要加上()才能执行
  • 参数不同。apply的参数第二个是数组,callbind的参数需要挨个写 {% endnote %}

数组去重

var arr = [1,2,3,5,2,1,4,4]
// 第一种使用ES6语法
const tempList2 = [...new Set(arr)];
// 第二种使用indexOf
function uniqe(arr){
    var tempList = [];
    for(let i=0;i<arr.length;i++){
        if(tempList.indexOf(arr[i]) == -1){
            tempList.push(arr[i])
        }
    }
    return tempList;
}

const tempList = uniqe(arr);

// 第三种使用 filter 方法去重
const array = [1, 2, 3, 3, 4, 4, 5];

const uniqueArray = array.filter((item, index) => array.indexOf(item) === index);

console.log(uniqueArray); // [1, 2, 3, 4, 5]


console.log(tempList,'去重后的数组。。')
console.log(tempList2,'去重后的数组。。')

找出多维数组的最大值

// 要求:找出多维数组中的最大值
var arr = [
    [3,6,8,9],
    [20,34,78,90],
    [1000,20399,390,700]
]

function findMax(arr){
    var tempList = [];
    arr.forEach((item,index) => {
        tempList.push( Math.max(...item) )
    })
    return tempList
}

console.log(findMax(arr),'xxx')

给字符串新增一个自定义方法

// 要求:给字符串新增方法
// 例如:"hello".addStr("word") ---> 输出 "helloword"

// 给string原型上添加方法即可
String.prototype.addStr = function(str){

    return this + str
}
const str = "hello".addStr("word");
console.log(str)

找出字符串中出现最多次数的字符以及次数

// 定义字符串
var str = "aaaaabbbbccdddssggass";

// 创建 Map 对象
var charMap = new Map()

// 遍历字符串中的所有字符
for (var i = 0; i < str.length; i++) {
    var c = str.charAt(i);
    // 如果字符 c 不在 Map 对象中,则将其作为键,并将值设为 1
    if (!charMap.has(c)) {
        charMap.set(c, 1);
    }
    // 如果字符 c 在 Map 对象中,则将其对应的值加 1
    else {
        var count = charMap.get(c);
        charMap.set(c, count + 1);
    }
}


// 找出出现次数最多的字符和次数
var maxChar = "";
var maxCount = 0;
for (var [char, count] of charMap) {
    if (count > maxCount) {
        maxChar = char;
        maxCount = count;
    }
}

// 输出出现次数最多的字符和次数
console.log( charMap);
console.log("出现次数最多的字符:" + maxChar);
console.log("出现次数:" + maxCount);

实现一个批量请求函数,能够限制并发量的

function batchRequest(urls, limit) {
  const requests = urls.map(url => ({ url, retry: 0 }));
  while (requests.length > 0) {
    // 使用 Promise.all() 函数限制并发量
    Promise.all(requests.splice(0, limit)).then(results => {
      // 处理批量请求的结果
      results.forEach((result, index) => {
        // 如果请求失败,并且重试次数小于 3,就重新发起请求
        if (!result.ok && requests[index].retry < 3) {
          requests[index].retry++;
          requests.push({ url: urls[index], retry: requests[index].retry });
        }
      });
    });
  }
}
const urls = [
  "https://jsonplaceholder.typicode.com/posts/1",
  "https://jsonplaceholder.typicode.com/posts/2",
  "https://jsonplaceholder.typicode.com/posts/3",
  "https://jsonplaceholder.typicode.com/posts/4",
  "https://jsonplaceholder.typicode.com/posts/5",
  "https://jsonplaceholder.typicode.com/posts/6",
  "https://jsonplaceholder.typicode.com/posts/7",
  "https://jsonplaceholder.typicode.com/posts/8",
  "https://jsonplaceholder.typicode.com/posts/9",
  "https://jsonplaceholder.typicode.com/posts/10"
];

// 调用批量请求函数,限制并发量为 3
batchRequest(urls, 3);

在上面的代码中,requests 是一个包含请求的数组。我们定义了一个变量 index,用来表示当前处理的请求在数组中的下标。所以 requests[index] 就是当前处理的请求。

在处理请求结果的过程中,我们为了记录重试次数,在每个请求对象上添加了一个属性 retry,用来表示当前请求的重试次数。所以 requests[index].retry 就是当前请求的重试次数。

这个重试次数的属性是我们自定义的,它并不是 JavaScript 语言的内置属性,只是为了方便实现重试逻辑而添加的一个属性。

js中的类型检测

4种js数据类型检测的方法:

  • typeof 操作符可以检测出基本数据类型,如字符串数值布尔值undefined,还有函数类型。但是对于 null 而言,typeof 操作符会返回 “object”,这是一个历史遗留问题。同时,typeof 操作符也不能判断引用数据类型,如数组、对象、函数等。

  • instanceof 运算符可以用于检测某个实例是否属于某个构造函数的原型链上。比如,arr instanceof Array 就可以判断 arr 是否为 Array 的实例。但是需要注意的是,instanceof 也不能完全判断数据类型,比如基本数据类型不支持 instanceof 操作符。

  • constructor 属性也可以用于检测实例的构造函数。比如,arr.constructor === Array 就可以判断 arr 是否为 Array 的实例。不过需要注意的是,constructor 属性可以被改写,也就会影响检测结果。

  • Object.prototype.toString.call(value) 可以用于检测所有数据类型,包括基本数据类型和引用数据类型。其中,Object.prototype.toString 方法返回一个表示当前对象类型的字符串,比如 [object Object] 表示对象类型,[object Array] 表示数组类型。可以通过 call 方法将要检测的数据传入 toString 方法中,从而获得数据的类型字符串。