面试常考js合集,狠狠拿捏金三银四

214 阅读12分钟

1.js数组上有哪些方法?

  1. 增加:push()、unshift()、splice()、concat()
  2. 删除:shift()、pop()、splice()、slice()
  3. 修改:reverse()、sort()
  4. 查找:indexOf()、lastIndexOf()、includes()、find()
  5. 转换:join()
  6. 迭代:forEach()、map()、filter()、some()、every()
  7. 其他:Array.from() 、Array.of()

方法介绍(因篇幅问题就不列举比较少见的方法):

shift       // 移除数组第一个元素并返回该元素的值
unshift     // 在数组的开头添加一个或多个元素,并返回新的长度
pop         // 移除数组最后一个元素并返回该元素的值
push        // 在数组的末尾添加一个或多个元素,并返回新的长度
splice      // 删除元素、插入元素、替换元素
reverse     // 反转数组元素的顺序
sort        // 对数组元素进行排序
concat      // 合并两个或多个数组,并返回一个新的数组
slice       // 选择数组的某个片段,并返回一个新的数组
indexOf     // 返回某个指定的元素在数组中的索引值,如果找不到该元素,则返回-1
lastIndexOf // 返回某个指定的元素在数组中的最后一个的索引值,如果找不到该元素,则返回-1
includes    // 判断某个元素是否在数组中,返回true或false
forEach     // 遍历数组的所有元素并执行回调函数
map         // 遍历数组的所有元素并执行回调函数,返回一个新的数组
filter      // 遍历数组的所有元素并执行回调函数,返回一个新的数组,新数组中的元素是通过回调函数的判断条件全部筛选出来的
some        // 遍历数组的所有元素并执行回调函数,返回true或false,只要有一个元素通过了回调函数的判断,就返回true
every       // 遍历数组的所有元素并执行回调函数,返回true或false,只要有一个元素没有通过回调函数的判断,就返回false
join        // 将数组的所有元素连接成一个字符串并返回这个字符串
Array.from() //从可迭代的对象构造一个新的数组实例
Array.of()   //创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型

如对常见数组方法详细感兴趣可以阅读笔者往期文章:

JavaScript常见数组方法详解

2. js字符串有哪些方法?

  1. 增加:concat()
  2. 删除:slice()、substring()、substr()
  3. 修改:replace()、toLowerCase()、toUpperCase()、trim()、padStart()、padEnd()
  4. 查找:indexOf()、lastIndexOf()、includes()、find()、endWith()、startWith()

方法介绍(同上):

charAt(index)           //返回指定索引处的字符。
concat(str)             //将另一个字符串连接到当前字符串。
endsWith(search)        //检查字符串是否以指定字符串结尾。
filter(callback)        //过滤字符串中的字符,只保留满足回调函数的字符。
includes(search)        //检查字符串是否包含指定字符串。
indexOf(search)         //返回指定字符串在字符串中的首次出现位置。
lastIndexOf(search)     //返回指定字符串在字符串中的最后一次出现位置。
match(regex)            //根据正则表达式匹配字符串。
repeat(count)           //将字符串重复指定次数。
replace(regex, str)     //用另一个字符串替换所有匹配正则表达式的字符。
search(regex)           //根据正则表达式搜索字符串。
slice(start, end)       //返回字符串的一部分。
split(separator, limit) //根据指定的分隔符将字符串分割成子字符串。
substring(start, end)   //返回字符串的一部分,范围从指定的start到指定的end。
toLocaleLowerCase()     //将字符串转换为本地小写。
toLocaleUpperCase()     //将字符串转换为本地大写。
toLowerCase()           //将字符串转换为小写。
toUpperCase()           //将字符串转换为大写。
trim()                  //删除字符串首尾的空白字符。

3. 谈谈js中的类型转换机制

是什么?

js中有原始类型和引用类型:

  • 原始类型:number、string、symbol、boolean、null、undefined、BigInt
  • 引用类型:object、function、array、Date、RegExp( 正则表达式)、Set、Map
转化方法?
  • 显示类型转换:Number()、String()、Boolean()、parseInt()、JSON.stringify()、JSON.parse()
  • 隐式类型转换:比较运算符( > < = == === != 等)、算数运算符(+ - * / %等)、条件判断语句
  1. 字符串和数字相加:当一个字符串和一个数字相加时,js会将数字隐式转换为字符串,然后进行字符串拼接。
var num = 10;
var str = "20";
var result = num + str; // 结果为 "1020"
  1. 比较操作:在使用比较操作符(如==、>、<等)比较不同数据类型时,js会进行隐式类型转换来使它们具有相同的数据类型后再进行比较。
console.log(10 == "10"); // 输出 true
console.log(10 > "5"); // 输出 true
  1. 逻辑运算:在使用逻辑运算符(如&&、||)时,js会将非布尔值转换为布尔值来进行逻辑运算。
var result1 = "hello" && "world"; // 结果为 "world"
var result2 = "" || "default"; // 结果为 "default"
  1. 数学运算:在使用数学运算符时,js会将非数字类型转换为数字类型来进行运算。
var result = "10" - 5; // 结果为 5
由此可以抛出的思考题: [] == ![] 结果为true还是false?

在ES6中,[] == ![] 的结果是 true,实际上它可以通过类型转换来解释:

1.首先,让我们分析一下 ![]。! 是逻辑非操作符,它会将操作数转换为布尔值并取反。[]是一个对象,而所有对象在js中都会被视为真值。在逻辑上,对象代表着“存在”,所以它会被转换为布尔值 true,然后取反就变成了 false。

2.接着,我们来看 [] == false。当使用双等号进行比较时,如果两边的操作数类型不同,js 会尝试对它们进行类型转换。左边是一个对象(数组),右边是一个布尔值,js 将布尔值转换为数字,即 false 被转换为 0。

3.综上所述,[] == ![] 可以转换为 [] == false,进而转换为 [] == 0。接下来,js 会尝试对数组和数字进行比较,此时会将数组转换为字符串,即 '',然后再将其转换为数字,即 0。所以最终的比较变成了 0 == 0,因此结果为 true。

上面的转换步骤如下:

[] == ![]
[] == !true
[] == false
'' == false
0 == 0

这种行为可能会带来一些混淆,最好使用严格相等操作符 === 来避免类型转换带来的意外结果。

image.png

如对js中的原始类型和引用类型Set、Map以及类型转换感兴趣可以阅读笔者往期文章:

JavaScript中的类型判断(一)——原始类型和引用类型

JavaScript中的类型判断(二)—— instanceof与Object.prototype.toString.call()

JavaScript中的类型判断(三)—— 用于判定对象类型的4种方法

JavaScript中的类型判断(附)—— 经典面试题之手动实现instanceof方法

JavaScript中的Set详解

JavaScript中的Map详解

4. == 和 === 的区别?

  • == 是比较两个值是否相等,不考虑类型,=== 是比较两个值和类型是否相等
  • == 存在类型转换
  • === 不存在类型转换

5. 深拷贝和浅拷贝的区别?如何实现一个深拷贝?

区别?
  • 浅拷贝:只拷贝一层对象,复制这一层对象中的原始值,如果有引用类型,拷贝的是引用地址,所以两个对象的引用地址是相同的,常见方法有:
    • create()
    • assign()
    • concat()
    • slice()
    • 解构
  • 深拷贝:层层拷贝,拷贝包括对象中的原始值和引用值,如果有引用类型,拷贝的是引用地址,但是两个对象的引用地址是不同的,常见方法有:
    • JSON.parse(JSON.stringify())
    • 函数库lodash的_.cloneDeep方法
    • MessageChannel()的postMessage()和onmessage()方法
实现?
// 浅拷贝
let obj = {
  a: 1,
  b: {n: 2}
}

let obj2 = shallowCopy(obj)

function shallowCopy(obj) {
  let newObj = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]
    } 
  }
  return newObj
}

obj.b.n = 3  // 改变原对象的引用类型的值

console.log(obj); // { a: 1, b: { n: 3 } }
console.log(obj2);// { a: 1, b: { n: 3 } }
// 因为两个对象的引用地址是相同的,所以都改变
// 深拷贝
let obj3 = {
  a: 1,
  b: {n: 2}
}

let obj4=deepCopy(obj3)

function deepCopy(obj) {
  if (obj === null) return obj; 
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (typeof obj !== "object") return obj;
  let newObj = new obj.constructor();
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      newObj[key] = deepCopy(obj[key]);
    }
  }
  return newObj;
}

obj3.b.n = 3 

console.log(obj3) // { a: 1, b: { n: 3 } }
console.log(obj4) // { a: 1, b: { n: 2 } }
// 新对象跟原对象不共享内存,所以后者不变

注:通常我们聊拷贝时只针对引用类型,JSON.parse(JSON.stringify(obj)) 方法存在一些限制。例如,它无法处理特殊的数据类型(如 undefinedfunction 和 Symbol),并且在处理循环引用时可能会导致无限循环的问题。

JavaScript拷贝详解

6. 请说说你对闭包的理解?

是什么?

当一个函数中的内部函数被拿到函数外部调用,又因为在js中内层作用域总是能访问外层作用域的,那么内部函数存在对外部函数中变量的引用,这些变量的集合称之为闭包

使用场景:
  1. 创建私有变量 (全局变量不易维护)
  2. 延长变量的生命周期
  3. 实现柯里化 (颗粒)
缺点:

会造成内存泄漏。

7. 什么是柯里化?

是什么?

将接受多个参数的函数转换成多个只接受一个参数(最初函数的第一个参数)的函数。

手撕:
// 示例
function sum(a, b, c) {
    return a + b + c;
}
// 实现
function curry(func) {
    return function curried(...args) {
        if (args.length >= func.length) {
            return func.apply(this, args);
        } else {
            return function (...moreArgs) {
                return curried.apply(this, args.concat(moreArgs));
            };
        }
    };
}


let curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 输出 6
console.log(curriedSum(1, 2)(3)); // 输出 6
console.log(curriedSum(1)(2, 3)); // 输出 6

8. 说说你对作用域的理解

是什么?

作用域是指在程序中定义变量或函数的区域,它决定了变量或函数的可见性和生存周期。

有哪些?
  1. 全局作用域:全局作用域是指在代码中任何位置都可以访问的范围,通常在代码最外层定义的变量会成为全局作用域的一部分。
var globalVar = 10;

function doSomething() {
    console.log(globalVar); // 可以在函数内部访问全局变量
}

console.log(globalVar); // 输出 10
  1. 函数作用域:函数作用域指的是在函数内部定义的变量只能在函数内部访问。
function doSomething() {
    var localVar = 20;
    console.log(localVar); // 可以在函数内部访问局部变量
}

console.log(localVar); // 无法访问局部变量,会报错
  1. 块级作用域:块级作用域是指使用花括号({})创建的范围,在 ES6 引入 let 和 const 后,可以在块级作用域内定义变量,而且这些变量只在块级作用域内有效。
if (true) {
    let blockVar = 30;
    console.log(blockVar); // 可以在块级作用域内访问变量
}

console.log(blockVar); // 无法访问块级作用域内的变量,会报错
往下聊
  • 作用域链:作用域只能从内到外的访问,这种访问规则形成的链状关系我们称为作用域链。

  • 词法作用域:指的是函数或变量定义的区域,函数定义的区域称为函数作用域,块级作用域称为块级作用域,全局作用域称为全局作用域。

var globalVar = 10;

function outerFunction() {
    var outerVar = 20;

    function innerFunction() {
        var innerVar = 30;
        console.log(innerVar); // 在内部函数中可以访问到内部变量
        console.log(outerVar); // 可以访问外部函数中的变量
        console.log(globalVar); // 可以访问全局变量
    }

    innerFunction();
}

outerFunction();

通过词法作用域,内部函数可以访问外部函数中的变量,甚至可以访问全局变量,这是因为函数在定义时就确定了其作用域链。

9. 说说你对原型的理解

是什么?
  • 显示原型指的是函数身上自带prototype属性,通常可以将一些书型盒方法添加在显示原型上,可供实例对象继承。
  • 隐式原型__proto__是对象这种结构上的一个属性,其中包含了创建该对象时,隐式继承的属性。
往下聊
  • 原型链:创建一个实例对象时,实例对象的隐式原型等于创建该实例对象的构造函数的显示原型,在js中对象的查找规则是先在对象中查找,找不到再去对象的隐式原型上查找,顺着隐式原型一层层往上找,直到找到null为止,这种查找规则我们就叫原型链。

  • 作用:可用来实现属性的继承。

10. 说说js中的继承

是什么?

在 js 中,继承是指一个对象获取另一个对象的属性和方法的过程,而 js 中的继承可以通过原型链实现。

  1. 原型链继承:js 中的对象可以通过原型(prototype)来继承另一个对象的属性和方法。每个对象都有一个指向其原型对象的内部链接,如果在当前对象上找不到某个属性或方法,解析器会沿着原型链向上查找,直到找到为止。
// 父类构造函数
function Animal(name) {
    this.name = name;
}

// 在父类原型上定义方法
Animal.prototype.sayName = function() {
    console.log("My name is " + this.name);
};

// 子类构造函数
function Dog(name, breed) {
    Animal.call(this, name); // 调用父类构造函数,继承父类属性
    this.breed = breed;
}

// 子类通过原型链继承父类方法
Dog.prototype = new Animal();

var myDog = new Dog("Buddy", "Golden Retriever");
myDog.sayName(); // 输出 "My name is Buddy"
  1. ES6 中的 class 继承:ES6 引入了 class 和 extends 关键字来更方便地实现继承。
class Animal {
    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log("My name is " + this.name);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // 调用父类构造函数,继承父类属性
        this.breed = breed;
    }
}

let myDog = new Dog("Buddy", "Golden Retriever");
myDog.sayName(); // 输出 "My name is Buddy"
加分项:

在 js 中,箭头函数没有自己的 thisargumentssupernew.target。箭头函数会继承外层作用域的上下文,因此它们无法像普通函数那样使用 this 来引用自身,也无法通过 new 关键字来调用创建实例。所以,箭头函数本身并不具有继承的能力,它们更适合用于简单的函数或者作为回调函数。

最后

相信看到这里就会有许多uu们明白了这一篇面经是循序渐进的,当面试官问我们有关 js 中的继承时,我们就可以回答上述内容并补充一些相关概念,比如原型、原型链、作用域等,让面试官给我们狠狠地加分,最后也祝愿uu们成功上岸!(跪求点赞+收藏,还有下一弹哦)