经典面试题:JS系列(一)

877 阅读7分钟

前言

今天给大家要讲是js方面的面试题:分别从js中的数组,字符串,类型转换机制,深浅拷贝和闭包这几个方面来介绍。让大家更加饱满的回答面试官的问题。

正文

js中数组常用的方法有哪些?

常用方法总结(我们按照增删改查的顺序来就不会乱,能便于我们说出更多的方法)

增加元素
  • push():向数组末尾添加一个或多个元素,并返回新的长度。
  • unshift():向数组开头添加一个或多个元素,并返回新的长度。
  • splice():在指定位置插入元素。
  • concat():用于合并两个或多个数组。
删除元素
  • pop():删除数组的最后一个元素并返回它。
  • shift():删除数组的第一个元素并返回它。
  • splice():从数组中删除元素。
  • slice():返回一个新数组,包含从开始到结束(但不包括结束)的数组的一部分副本。
查找元素
  • indexOf():返回数组中某个元素的第一个匹配项的索引。
  • includes():检查数组中是否存在某个元素。
  • lastIndexOf():返回数组中某个元素的最后一个匹配项的索引。
  • find():返回数组中满足条件的第一个元素的值。
迭代
  • forEach():遍历数组中的每个元素。
  • map():创建一个新数组,其结果是调用一个提供的函数处理过的原始数组中的每个元素。
  • reduce():对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
  • every():测试数组中的所有元素是否都通过了被提供的函数测试。
  • some():测试数组中是否有至少一个元素通过了被提供的函数测试。
  • filter():创建一个新数组,其包含通过测试的所有元素。
转换
  • join():把数组的所有元素放入一个字符串。
  • reverse():反转数组中元素的顺序。
  • sort():按字典顺序对数组的元素进行排序。

示例

// 以下操作均基于经过修改后的数组 `arr`。
let arr = [1, 2, 3, 4, 5, 6];

// 添加元素
arr.splice(3, 0, 0); // 在索引3的位置插入0  [1, 2, 3, 0, 4, 5, 6]
arr.splice(2, 1);    // 删除索引2的元素
console.log(arr);    // 输出: [1, 2, 0, 4, 5, 6]

// 查找
let res1 = arr.find(val => val < 1);
console.log(res1);   // 输出: 0

// 映射
let newArr = arr.map(val => val * 2);
console.log(newArr); // 输出: [ 2, 4, 6, 8, 10, 12 ]

// 归约
let sum = arr.reduce((pre, item) => pre + item, 0);
console.log(sum);    // 输出: 21

// 检查
let res2 = arr.every(val => val < 10);
console.log(res2);   // 输出: true

// 测试
let res3 = arr.some(val => val < 10);
console.log(res3);   // 输出: true

// 过滤
let res4 = arr.filter(val => val % 2 === 0);
console.log(res4);   // 输出: [2, 4, 6]

// 反转
let arr2 = arr.reverse();
console.log(arr2);   // 输出: [ 6, 5, 4, 3, 2, 1 ]

js中字符串常用的方法有哪些?(一样的从增删改查这几个方面回答)

常用方法总结

增加
  • concat():连接两个或多个字符串。
删除
  • substring(start, end):提取字符串中的一部分。
  • slice(start, end):提取字符串中的一部分。
修改
  • replace():替换与正则表达式匹配的部分。
  • trim():删除字符串两端的空白字符。
  • trimLeft() / trimStart():删除字符串左侧的空白字符。
  • trimRight() / trimEnd():删除字符串右侧的空白字符。
  • toLowerCase():转换为小写。
  • toUpperCase():转换为大写。
查找
  • charAt(index):获取指定索引处的字符。
  • startsWith():检查字符串是否以指定的子字符串开头。
  • endsWith():检查字符串是否以指定的子字符串结尾。
  • indexOf():查找子字符串出现的位置。
  • lastIndexOf():查找子字符串最后一次出现的位置。
  • match():查找符合正则表达式的部分。
  • search():查找符合正则表达式的部分。
转换
  • split():把一个字符串分割成字符串数组。

示例

let str = 'abcd';

// 截取
let str2 = str.substring(1, 3);
console.log(str2);  // 输出: 'bc'

// 替换
let str3 = str.replace('a', 'A');
console.log(str3);  // 输出: 'Abcd'

// 补充
let str4 = str.padStart(10, '0');
console.log(str4);  // 输出: '0000000abcd'

let str5 = str.padEnd(10, '0');
console.log(str5);  // 输出: 'abcd000000'

谈谈js中的类型转换机制

是什么

js中有7种原始类型,和引用类型之分,在开发时有时候我们需要人为的将一个变量的类型转换为其他类型,这种转换称之为显示转化,有时候js引擎也会发生隐式转换

方式:

  • 显式转换:通过使用内置的转换函数如 Number()String()Boolean() 或 parseInt()parseFloat() 等。
  • 隐式转换:在某些特定情况下,JavaScript 会自动将一种类型转换为另一种类型,例如在比较运算符或算术运算中。

面试题

面试题

问题[] == ![] 的结果是 true 还是 false

  • [] 是一个空数组,它的类型是 object
  • ![] 是一个布尔表达式,![] 的结果是 false,因为一个非空数组在布尔上下文中被认为是 true,所以取反后是 false

逐步分析

让我们逐步分析 [] == ![] 的过程:

  1. [] == ![]

    • 转换过程:(!优先,先看右边的)
      1. ![] -> false -> ToNumber(false) -> 0

      2. []

      • ToPrimitive([], Number)

        • valueOf() -> [] (仍然是空数组)
        • toString() -> "" (空字符串)
        • ToNumber("") -> 0
    • 比较结果0 == 0 -> true

== VS ===

== 只判断值是否相等,不在乎类型,会发生隐式类型转换

===不发生类型转换(它不仅比较值,还比较类型)


聊聊js的拷贝问题

是什么

开发中经常面对一个要跟原对象得到一个新对象的场景,这种场景我们称之为拷贝

特点

  • 浅拷贝:只复制对象的第一层属性。
  • 深拷贝:完全复制对象及其所有嵌套的对象。

方法

  • 浅拷贝:

    • Object.assign()
    • Object.create()
    • {...obj}
    • [...arr]
    • arr.concat()
    • arr.slice()
    • arr.toReversed().reverse()

示例

function copy(obj){
    let obj1={};
    for(let i in obj){
        if(obj.hasOwnProperty(i)){
            obj1[i]=obj[i];
         }
    }
    return obj1;
}
let obj2=copy(obj);
console.log(obj2); 

  • 深拷贝:

    • JSON.parse(JSON.stringify(obj)): 无法处理 bigintSymbolundefined 类型, 无法处理循环引用。
    • structuredClone(): 无法处理函数、Symbol
    • 手动实现递归拷贝。
    • new MessageChannel(): 可以处理循环引用,但不适用于所有数据类型。(管道通信)

structuredClone()

const obj = {
    a: 1,
    b: 2,
    c: {
        d: 3
    }
};

const clonedObj = structuredClone(obj);
obj.b.n = 20;
console.log(clonedObj);   // 输出: { a: 1, b: 2, c: { d: 3 } }

手动实现递归拷贝示例

function deepClone(obj) {
    const newObj = {};
    for (const key in obj) {
        if (typeof obj[key] === 'object' && obj[key] !== null) {
            newObj[key] = deepClone(obj[key]);
        } else {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

const obj2 = deepClone(obj);

new MessageChannel()

const obj={
    a:1,
    b:2,
    c:{
        d:3
    }
}

function deepClone(obj){
    return new Promise((resolve)=>{
        const {port1,port2} =new MessageChannel()
        port1.postMessage(obj)
        port2.onmessage=(msg)=>{
            resolve(msg.data)
        }
    })
}
deepClone(obj).then((res)=>{
    obj.b.n=20
    console.log(res)   //输出:{ a: 1, b: 2, c: { d: 3 } }
})  

解析:在 deepClone 函数中,创建了一个 MessageChannel 实例,并通过 postMessage 方法将原始对象发送到一个端口 (port1),然后在另一个端口 (port2) 上监听消息事件,当接收到消息时,解析得到的克隆对象并将其作为 Promise 的结果返回。最后,通过调用 deepClone(obj) 并等待 Promise 解析,我们可以获得一个与原始对象 obj 深度克隆的新对象。


说说闭包

定义

在JavaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。

示例

function foo(){
    var a = 1;
    function bar(){
        console.log(a);
    }
    return bar;
}
const baz=foo();  //输出1
baz();

image.png

解析:蓝色为全局环境,首先foo的声明,baz的声明,预编译结束,然后foo的调用,黄色为foo进栈,里面有bar的声明,foo函数被执行完毕后要出栈,但是内部有一个函数还没调用,所在旁边给它留下了一个小背包,里面有它所需要的变量a=1,foo出栈就打一个大大的×,然后baz的调用,导致绿色的bar进栈,通过作用域链找到小背包,所以输出1

优缺点

  • 优点:可以定义私有变量,防止全局变量污染,便于封装模块。
  • 缺点:可能导致内存泄漏。

应用场景

  • 防抖节流
  • 单例模式
  • 柯里化

总结

以上就是本文的内容,希望对您有所帮助!感谢您的阅读!