JS面试题整理
原型继承和类继承的区别?
"原型继承"和 ES6 引入的"类继承"本质上是同一套机制的两种表现形式。
function Animal(name) {
this.name = name;
}
// 通过prototype属性定义方法
Animal.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
// 继承实现需要三步: 1. call 2. create 3. constructor
function Dog(name, breed) {
Animal.call(this, name); // 借用构造函数,继承属性
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 原型继承方法
Dog.prototype.constructor = Dog; // 修复 constructor 指向
const dog = new Dog('Lucky', 'Labrador');
dog.sayHi(); // Hi, I'm LuckyES6 提供 class 和 extends 语法糖,本质仍然是基于原型链。class 定义的 methods 自动放在 prototype 上,extends 内部帮你做了 Object.create 那些事。
什么是 prototype、__proto__?
prototype:只有函数才有 prototype,当一个函数被用作构造函数(即通过 new 调用)时,prototype 属性用来为实例对象提供共享的属性和方法。
__proto__:所有对象都有该属性,指向对象原型,即构造函数的 prototype。
什么时候用:
prototype 是函数(构造函数/class)才有的,用来存放共享的方法、属性。
每个对象身上都有
__proto__,指向构造函数的 prototype(理解 instanceOf 的关键)。
举例说明:构造函数是造车工厂,prototype 是工厂提供的工具箱,每一辆车都是这套工具加工生产的。__proto__ 是每辆车的内心发问:"我是哪套工具产生的?"总是指向构造函数的 prototype。
原型链的指向是怎样的?
dog.__proto__→Dog.prototypeDog.prototype.__proto__→Animal.prototypeAnimal.prototype.__proto__→Object.prototype最终到
null
instanceof 的实现原理?
function myInstanceOf(obj, Constructor) {
// 基本类型直接返回 false
if (typeof obj !== 'object' || obj === null) return false;
let proto = obj.__proto__;
const prototype = Constructor.prototype;
while (proto) {
if (proto === prototype) return true;
proto = proto.__proto__;
}
return false;
}new 对象的时候发生了什么?
四步:
创建一个新对象
obj。把
obj.__proto__指向构造函数的prototype。执行构造函数,绑定
this为obj,往obj上添加属性。返回新对象(或构造函数返回的对象)。
手写实现:
function myNew(Constructor, ...args) {
// 1. 创建空对象
let obj = {};
// 2. 连接原型链
obj.__proto__ = Constructor.prototype;
// 3. 执行构造函数
let result = Constructor.apply(obj, args);
// 4. 返回
return result instanceof Object ? result : obj;
}什么是闭包?
闭包本质是函数对外部作用域变量的引用,延长作用域链。
使用场景:
封装私有变量
在异步环境中"记住"某些状态
函数工厂(生成带记忆能力的函数)
// 模拟私有变量
function Person(name) {
let _age = 0; // 私有变量
return {
getName() {
return name;
},
getAge() {
return _age;
},
growUp() {
_age++;
}
};
}
// 异步环境记住状态
for (var i = 1; i <= 3; i++) {
(function(n) {
setTimeout(() => {
console.log("i =", n);
}, 1000);
})(i);
}
// 1秒后输出: i = 1, i = 2, i = 3
// 函数工厂
function createCounter(start = 0) {
let count = start;
return function() {
count++;
return count;
};
}
const counter1 = createCounter(10);
const counter2 = createCounter(100);什么是 IIFE?使用场景有哪些?
使用场景:
创建独立作用域,库用来隔离变量
立即运行一次逻辑(执行初始化代码)
模块化之前,通过 IIFE 封装库
Boolean 和 boolean 的区别?
boolean 是原始数据类型,只有两个值 true 和 false。
Boolean 是 JavaScript 的一个内置对象构造函数,用于创建 Boolean 包装对象。但通常不推荐作为构造对象来使用,更多的是作为普通函数用来将任意值转换成 boolean。
== 的判断规则?
x == y,当二者类型相同时,直接按 === 来比较,但不完全等价:NaN == NaN 是 false。
Number == String→ 把 String 转成 NumberBoolean == Anything→ 把 Boolean 转成 Numbernull == undefined→ 它们俩相等,且只彼此相等Object == Primitive→ 对象通过valueOf()或toString()转为原始值,再比较NaN == anything→ 永远是 false
注意:== 和 if(value) 判断逻辑不一致,因为 if(value) 不涉及隐式转换。
+ 运算符的隐式转换规则?
如果任意一边是字符串,优先转为字符串并拼接;否则尝试转数字。
WeakMap 的键为什么不能是基本类型?
WeakMap 的设计目的是存放临时数据,避免内存泄漏。
为了垃圾回收,它的键都是弱引用,且不可枚举。
值类型不存在"引用",因此无法实现弱引用。
如何用 WeakMap 封装私有属性?
const _privateData = new WeakMap();
class Person {
constructor(name) {
_privateData.set(this, { name });
}
getName() {
return _privateData.get(this).name;
}
}
const p = new Person("Alice");
console.log(p.getName());普通函数和箭头函数的区别?
普通函数:
this是动态绑定的有自己的
arguments对象(类数组)可以作为构造函数被
new调用有
prototype属性可以作为生成器
箭头函数:
this指向定义时的上下文中的this,自己没有this没有
arguments不能作为构造函数
没有
prototype不能作为生成器(不能有
yield)
this 取值的四个场景?
调用方式 | this 指向 |
|---|---|
直接调用 | 严格模式: |
对象方法调用 |
|
构造函数调用 |
|
通过 |
|
call / apply / bind 的区别?
call:调用函数,并指定
this,参数逐个传入。apply:调用函数,并指定
this,参数以数组形式传入。bind:不会立即调用,而是返回一个新函数,
this被永久绑定。
call / apply 更多用于方法借用:Array.prototype.slice.call(arguments)
TS 中 Exclude 和 Omit 的区别?
Exclude<T, U>:从联合类型 T 中排除掉可以赋值给 U 的成员。
Omit<T, K>:从对象类型 T 中移除某些键 K。
如何检测对象是否循环引用?
"循环引用"就是对象的某个属性(直接或间接)又指向了对象本身:
const obj = {};
obj.self = obj;方法1:JSON.stringify 本身不能处理循环引用,会抛错,但可以配合 try-catch "侧面检测"。
方法2:Set 不能存储重复的值,把对象放进 Set,并遍历对象尝试放入 Set。为了更好的垃圾回收可以用 WeakSet。
function hasCycle(obj) {
const seen = new WeakSet();
function detect(value) {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return true; // 检测到循环
}
seen.add(value);
for (let key in value) {
if (detect(value[key])) {
return true;
}
}
}
return false;
}
return detect(obj);
}JS 中有哪些方法可以退出循环?
方法 | 作用范围 | 效果 |
|---|---|---|
| 循环 | 结束整个循环 |
| 循环 | 跳过本次,进入下次迭代 |
| 函数内部循环 | 结束函数,循环自然退出 |
| 任意 | 抛异常,中断循环 |
| 数组迭代方法 | 逻辑条件成立后退出迭代 |
如何使对象支持 for...of 迭代?
obj[Symbol.iterator] = function* () {
for (const key of Object.keys(this)) {
yield [key, this[key]];
}
};Proxy 和 Object.defineProperty 的区别?
Object.defineProperty 是给对象设置 get/set,Proxy 会代理整个对象,可以拦截多种操作。
特性 | Object.defineProperty | Proxy |
|---|---|---|
粒度 | 针对单个属性 | 针对整个对象 |
拦截范围 | 仅限 get / set | 支持 13 种操作(get、set、has、deleteProperty、ownKeys、apply、construct 等) |
新增/删除属性 | 无法拦截 | 可以拦截 |
数组索引/长度变化 | 无法拦截 | 可以拦截 |
继承/原型链操作 | 无法拦截 | 可以拦截 |
函数调用 | 无法拦截 | apply 可拦截函数调用 |
性能 | 高(更轻量) | 较低(代理更复杂) |
a.b.c.d 和 a['b']['c']['d'] 的区别?
点语法:要求每一级属性名必须是合法的标识符(字母、数字、下划线、美元符号,且不能以数字开头),且属性名固定写死。
方括号语法:属性名是一个字符串,可以是任意字符,属性名可以是变量动态访问。
use strict 是什么,有什么用?
"use strict" 是 JavaScript 的严格模式指令,用来让代码在更严格的语法和运行环境下执行。
可以全局开启(整个脚本)或函数内部开启。ES6 module 默认就是严格模式,无需手动加。
行为类别 | 非严格模式 | 严格模式 |
|---|---|---|
全局变量 | 未声明直接赋值会自动创建全局变量 | 未声明直接赋值会报 ReferenceError |
重复参数 | 函数参数可重复 | 报语法错误 |
this 指向 | 函数中 | 函数中 |
删除属性 | 可删除任意属性(即使不可配置) | 删除不可配置属性报 TypeError |
eval / arguments | 可以随意使用 |
|
八进制 | 支持以 | 八进制语法报错( |
保留关键字 | 可以作为变量名 | 某些未来关键字不能用作变量名 |
在 DOM 中,如何判定 a 元素是否是 b 元素的子元素?
方法 | 是否推荐 | 特点 |
|---|---|---|
| ✅ 推荐 | 简洁、直观、性能好、现代浏览器支持 |
| ⚪ 可用 | 更底层,支持复杂节点关系判断 |
遍历 parentNode | ⚪ 可用 | 兼容旧浏览器,不依赖 API |
let current = a.parentNode;
while (current) {
if (current === b) {
console.log("a 是 b 的子元素");
break;
}
current = current.parentNode;
}CommonJS 的本质是什么?
通过了解 require 函数的实现可以彻底搞懂 CommonJS:
const moduleCache = {};
function require(modulePath) { // 1. 解析模块的绝对路径 const _filename = resolveModulePath(modulePath);
// 2. 检查模块是否已存在于缓存中 if (moduleCache[_filename]) { return moduleCache[_filename]; }
// 3. 真正运行模块代码的辅助函数 function _require(exports, require, module, _filename, _dirname) { // 目标模块的代码将在这个函数内执行... }
// 4. 准备参数 const module = { exports: {}, // 模块的导出对象 };
const exports = module.exports; const _dirname = getDirName(_filename);
// 5. 调用模块运行函数 _require.call(exports, exports, require, module, _filename, _dirname);
// 6. 缓存 module.exports moduleCache[_filename] = module.exports;
再面对修改 module / module.exports / this 的题,以及关于缓存的题都参考这个。