搞前端开发面试的时候,JavaScript 那可是重中之重,面试官肯定会问一些 JS 知识。今天就给大家好好唠唠 JS 面试里常见的考点,希望能帮大家在面试里顺顺利利,脱颖而出。
一、数据类型
类型分类
JavaScript 的数据类型分为 原始类型 和 引用类型:
- 原始类型:number、string、boolean、undefined、null(ES6 之前 5 种)、symbol、bigint。
- 引用类型:object、function、array、Date(ES6 新增)、Map、Set、RegExp 等。
类型判断
1. typeof
- 优点:快速判断原始类型(如
number
、string
等)。 - 缺点:
typeof null
返回object
(错误)。- 对引用类型,除
function
外均返回object
。
console.log(typeof 'hello'); // string
console.log(typeof {}); // object
console.log(typeof null); // object ❌
2. instanceof`
判断对象是否是某个构造函数的实例:
console.log([] instanceof Object); // true
console.log(new Date() instanceof Date); // true
3. Object.prototype.toString()
通过 [[Class]]
内部属性精准判断类型:
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(/abc/)); // [object RegExp]
- 对象上的 toString() 返回 '[object 变量类型]'格式的字符串
- 数组上的 toString() 返回 数组元素以逗号拼接的字符串
- 其他类型的 toString() 返回 字符串字面量
类型转换
显式转换
使用 Number()
、String()
、Boolean()
等方法:
console.log(Number('123')); // 123
console.log(String(456)); // "456"
隐式转换
在运算符或条件判断中自动触发:+ - * / == != ! > < >= <= if while
1. 原始类型 -> 原始类型
console.log(1 + '2'); // "12"(数字转字符串)
console.log(true && 123); // 123(布尔值转数字)
2. 引用类型 -> 原始类型
- (对象 ==> 数字
ToNumber({})
) 或者 (对象 ==> 字符串ToString({})
)ToNumber
和ToNumber
只能处理原始类型- 当它们遇到引用类型内部执行
ToPrimitive({})
ToPrimitive 规则
当需要将对象转换为原始值时,按以下步骤执行:
-
转换为数字:
- 调用对象的
valueOf()
,若返回原始值则结束。 - 调用对象的
toString()
,若返回原始值则结束。 - 抛出错误。
- 调用对象的
-
转换为字符串:
- 调用对象的
toString()
,若返回原始值则结束。 - 调用对象的
valueOf()
,若返回原始值则结束。 - 抛出错误。
- 调用对象的
const obj = {
valueOf() { return 123; },
toString() { return 'abc'; }
};
console.log(+obj); // 123(转数字调用 valueOf)
console.log(obj + ''); // "abc"(转字符串调用 toString)
二、this 指向
默认绑定
函数独立调用时:
- 浏览器环境指向
window
,Node 环境指向undefined
。
var a = 1;
function foo() {
console.log(this.a);
}
foo(); // 浏览器输出 1,Node 输出 undefined
隐式绑定
函数被对象调用时,this
指向该对象:
let obj = {
a: 2,
fn: foo
};
obj.fn(); // 输出 2
隐式丢失
当函数被一连串对象调用时,this
指向最近的调用对象:
const obj = {
fn: function() {
return function() {
console.log(this); // window(独立调用时)
};
}
};
obj.fn()(); // this 指向 window,而非 obj
显式绑定
通过 call
、apply
、bind
明确指定 this
:
let obj3 = {
a: 2
};
function foo() {
console.log(this.a);
}
foo.call(obj3); // 输出2
call
、apply
、bind
的区别
call
:接收参数列表,直接调用函数。apply
:接收参数数组,直接调用函数。bind
:返回新函数,新函数可接收额外参数:
function foo(a, b) {
console.log(this, a, b);
}
const obj = { name: 'obj' };
foo.call(obj, 1, 2); // obj 1 2
foo.apply(obj, [3, 4]); // obj 3 4
const bound = foo.bind(obj);
bound(5, 6); // obj 5 6
三、==
vs ===
==
的隐式转换
会尝试类型转换后再比较:
console.log(1 == '1'); // true(数字转字符串)
console.log('' == 0); // true(空字符串转数字为 0)
console.log([1] == '1'); // true(数组转字符串为 "1")
===
的严格比较
直接比较值和类型:
console.log(1 === '1'); // false(类型不同)
console.log({} === ![]); // false(对象与布尔值比较)
四、闭包
定义
根据词法作用的规则,内部函数总是可以访问其外部函数中的变量的。 在函数执行完毕后,它的执行上下文对象要被销毁
当函数 A 内部声明了一个函数 B,函数 B 被拿到 A 的外部调用,为了同时满足以上两个规则,函数 A 的执行执行上下文被销毁后会遗留下来一个对象空间,用于存放函数 B 有权访问的所有变量。这个对象空间被称为闭包。
function foo() {
let a = 1;
return function () {
console.log(a);
};
}
const bar = foo();
bar(); // 输出1
风险
闭包可能导致内存泄漏:
function createCounter() {
let count = 0;
return function() {
return ++count; // 闭包保留 count 的引用
};
}
const counter = createCounter();
// 若 counter 长期存在,count 不会被回收
五、拷贝
浅拷贝
新对象受原对象影响:
let obj = { a: 1, b: { n: 2 } };
let newObj = Object.assign({}, obj);
obj.b.n = 200;
console.log(newObj.b.n); // 200
常用方法:
Object.assign()
用于将一个或多个源对象的所有可枚举属性复制到目标对象。它会返回目标对象。
let originalObj = { a: 1, b: { c: 2 } };
let copiedObj = Object.assign({}, originalObj);
// 修改原对象的嵌套对象属性
originalObj.b.c = 3;
console.log(copiedObj.b.c); // 输出: 3,说明浅拷贝受原对象影响
Object.create()
创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
。
let original = { x: 10, y: { z: 20 } };
let copy = Object.create(Object.getPrototypeOf(original), Object.getOwnPropertyDescriptors(original));
// 修改原对象的嵌套对象属性
original.y.z = 25;
console.log(copy.y.z); // 输出: 25,浅拷贝受原对象影响
- 展开运算符
...
将一个可迭代对象(如数组或对象)展开为其各个元素。
let originalArr = [1, { value: 2 }];
let copiedArr = [...originalArr];
// 修改原数组中嵌套对象的属性
originalArr[1].value = 3;
console.log(copiedArr[1].value); // 输出: 3,浅拷贝受原对象影响
let originalObj = { a: 4, b: { c: 5 } };
let copiedObj = { ...originalObj };
// 修改原对象的嵌套对象属性
originalObj.b.c = 6;
console.log(copiedObj.b.c); // 输出: 6,浅拷贝受原对象影响
Array.prototype.slice()
slice()
方法返回一个新的数组对象,这一对象是一个由 begin
和 end
决定的原数组的浅拷贝(包括 begin
,不包括 end
)。原始数组不会被改变。
let originalArray = [1, { num: 2 }];
let slicedArray = originalArray.slice();
// 修改原数组中嵌套对象的属性
originalArray[1].num = 4;
console.log(slicedArray[1].num); // 输出: 4,浅拷贝受原对象影响
Array.prototype.concat()
用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
let arr1 = [1, { value: 2 }];
let arr2 = [3];
let combinedArray = arr1.concat(arr2);
// 修改原数组中嵌套对象的属性
arr1[1].value = 5;
console.log(combinedArray[1].value); // 输出: 5,浅拷贝受原对象影响
深拷贝
新对象不受原对象影响:
JSON.parse
- 递归拷贝
structuredClone
// 方法 1:JSON.parse(不支持 symbol、bigint)
const newObj = JSON.parse(JSON.stringify(obj));
// 方法 2:递归拷贝
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
clone[key] = deepClone(obj[key]);
}
return clone;
}
// 方法 3:structuredClone(兼容性需注意)
const newObj = structuredClone(obj);
六、原型与继承
原型链
每个对象都有 __proto__
(隐式原型),指向其构造函数的 prototype
(显式原型)。属性查找遵循 __proto__
链,直到 null
:
function Parent() {}
Parent.prototype.say = function() { console.log('Hello'); };
const child = Object.create(Parent.prototype);
child.__proto__ === Parent.prototype; // true
child.say(); // "Hello"
new
的实现
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args); //调整 this 指向
return result instanceof Object ? result : obj;
}
继承方式
1. 原型链继承:
- 子类无法给父类传参。
- 示例:
function Parent(name) { this.name = name; } Parent.prototype.say = function() { console.log('Hello'); }; Child.prototype = new Parent();
2. 借用构造函数继承:
- 无法继承父类的原型方法。
- 示例:
function Child(name) { Parent.call(this, name); }
3. 组合继承:
- 父类构造函数执行两次(父类实例化两次)。
- 示例:
Child.prototype = new Parent(); // 父类第一次执行 function Child(name) { Parent.call(this, name); } // 父类第二次执行
4. 寄生组合继承:
- 通过
Object.create
避免父类重复执行。 - 示例:
function inheritPrototype(subType, superType) { const prototype = Object.create(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } inheritPrototype(Child, Parent);
5. ES6 类继承:
- 使用
extends
和super()
:class Child extends Parent { constructor(name) { super(name); // 调用父类构造函数 this.sex = 'male'; } }
七、var
、let
、const
关键区别
特性 | var | let / const |
---|---|---|
块级作用域 | 无 | 有 |
声明提升 | 有 | 无(存在暂时性死区) |
重复声明 | 允许 | 不允许 |
变量提升 | 变量和值都提升 | 变量提升,值为 undefined |
全局变量挂载 | 挂载到 window | 不挂载 |
暂时性死区
在块级作用域内,变量声明前访问会报错:
if (true) {
console.log(temp); // ReferenceError(暂时性死区)
let temp = 123;
}
八、常见问题
0.1 + 0.2 的精度丢失
console.log(0.1 + 0.2); // 0.30000000000000004
// 解决方案:使用整数运算或第三方库(如 decimal.js)
超过 JS 最大安全数
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
// 处理大数:使用 BigInt 或字符串拼接
const bigInt = 4856315546156412n + 15546156412n; // 使用 BigInt
通过以上内容,你已经掌握了 JS 面试的基础知识点。祝你面试顺利,斩获心仪的 offer!