1.js数组上有哪些方法?
- 增加:push()、unshift()、splice()、concat()
- 删除:shift()、pop()、splice()、slice()
- 修改:reverse()、sort()
- 查找:indexOf()、lastIndexOf()、includes()、find()
- 转换:join()
- 迭代:forEach()、map()、filter()、some()、every()
- 其他: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() //创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型
如对常见数组方法详细感兴趣可以阅读笔者往期文章:
2. js字符串有哪些方法?
- 增加:concat()
- 删除:slice()、substring()、substr()
- 修改:replace()、toLowerCase()、toUpperCase()、trim()、padStart()、padEnd()
- 查找: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()
- 隐式类型转换:比较运算符( > < = == === != 等)、算数运算符(+ - * / %等)、条件判断语句
- 字符串和数字相加:当一个字符串和一个数字相加时,js会将数字隐式转换为字符串,然后进行字符串拼接。
var num = 10;
var str = "20";
var result = num + str; // 结果为 "1020"
- 比较操作:在使用比较操作符(如==、>、<等)比较不同数据类型时,js会进行隐式类型转换来使它们具有相同的数据类型后再进行比较。
console.log(10 == "10"); // 输出 true
console.log(10 > "5"); // 输出 true
- 逻辑运算:在使用逻辑运算符(如&&、||)时,js会将非布尔值转换为布尔值来进行逻辑运算。
var result1 = "hello" && "world"; // 结果为 "world"
var result2 = "" || "default"; // 结果为 "default"
- 数学运算:在使用数学运算符时,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
这种行为可能会带来一些混淆,最好使用严格相等操作符 === 来避免类型转换带来的意外结果。
如对js中的原始类型和引用类型Set、Map以及类型转换感兴趣可以阅读笔者往期文章:
JavaScript中的类型判断(一)——原始类型和引用类型
JavaScript中的类型判断(二)—— instanceof与Object.prototype.toString.call()
JavaScript中的类型判断(三)—— 用于判定对象类型的4种方法
JavaScript中的类型判断(附)—— 经典面试题之手动实现instanceof方法
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)) 方法存在一些限制。例如,它无法处理特殊的数据类型(如 undefined、function 和 Symbol),并且在处理循环引用时可能会导致无限循环的问题。
6. 请说说你对闭包的理解?
是什么?
当一个函数中的内部函数被拿到函数外部调用,又因为在js中内层作用域总是能访问外层作用域的,那么内部函数存在对外部函数中变量的引用,这些变量的集合称之为闭包
使用场景:
- 创建私有变量 (全局变量不易维护)
- 延长变量的生命周期
- 实现柯里化 (颗粒)
缺点:
会造成内存泄漏。
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. 说说你对作用域的理解
是什么?
作用域是指在程序中定义变量或函数的区域,它决定了变量或函数的可见性和生存周期。
有哪些?
- 全局作用域:全局作用域是指在代码中任何位置都可以访问的范围,通常在代码最外层定义的变量会成为全局作用域的一部分。
var globalVar = 10;
function doSomething() {
console.log(globalVar); // 可以在函数内部访问全局变量
}
console.log(globalVar); // 输出 10
- 函数作用域:函数作用域指的是在函数内部定义的变量只能在函数内部访问。
function doSomething() {
var localVar = 20;
console.log(localVar); // 可以在函数内部访问局部变量
}
console.log(localVar); // 无法访问局部变量,会报错
- 块级作用域:块级作用域是指使用花括号({})创建的范围,在 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 中的继承可以通过原型链实现。
- 原型链继承: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"
- 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 中,箭头函数没有自己的 this、arguments、super 或 new.target。箭头函数会继承外层作用域的上下文,因此它们无法像普通函数那样使用 this 来引用自身,也无法通过 new 关键字来调用创建实例。所以,箭头函数本身并不具有继承的能力,它们更适合用于简单的函数或者作为回调函数。
最后
相信看到这里就会有许多uu们明白了这一篇面经是循序渐进的,当面试官问我们有关 js 中的继承时,我们就可以回答上述内容并补充一些相关概念,比如原型、原型链、作用域等,让面试官给我们狠狠地加分,最后也祝愿uu们成功上岸!(跪求点赞+收藏,还有下一弹哦)