本文的内容是JS 底层解析,覆盖作用域、原型链、继承、this、事件模型、类型判断等核心问题。每道题附带原理说明 + 代码示例 + 面试要点,建议收藏反复记忆。
一、说说你对 JS 作用域的理解
是什么
作用域就是变量和函数生效的区域,决定了变量在哪些范围内可以被访问。
JS 作用域分为三种:
| 类型 | 说明 | 创建方式 |
|---|---|---|
| 全局作用域 | 最外层区域,全局变量随处可访问 | 顶层声明的变量 |
| 函数作用域 | 函数内部,外部无法访问 | 函数声明 |
| 块级作用域 | {} 内部,let/const 专属 | let/const + {} |
核心规则
// 1. 内层可以访问外层,外层不能访问内层
let global = '全局';
function outer() {
let outerVar = '外层';
function inner() {
console.log(outerVar); // ✅ 内层访问外层
}
inner();
console.log(innerVar); // ❌ ReferenceError
}
// 2. 块级作用域
{
let a = 1;
var b = 2; // var 没有块级作用域
}
console.log(a); // ❌ ReferenceError(let 受块级作用域限制)
console.log(b); // ✅ 2(var 泄漏到外层)
// 3. 经典面试题:var vs let
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3 3 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 0 1 2
}
词法作用域
JS 采用词法作用域(静态作用域):函数的作用域在定义时就确定了,而不是调用时。
let name = 'global';
function foo() {
console.log(name); // 'global',定义时的环境域
}
function bar() {
let name = 'bar';
foo(); // 调用时 name 仍是 'global'
}
bar(); // 输出 'global'
作用域查找规则
1. 先在当前作用域查找
2. 当前没有 → 去父作用域查找
3. 层层往上,直到全局作用域
4. 全局也没有 → ReferenceError
面试要点
var没有块级作用域,let/const有——这是 ES6 引入块级作用域的原因- 词法作用域:函数的作用域在定义时确定,与调用位置无关
- 作用域链:内层访问外层变量的查找路径,层层往外直到全局
let经典题:for循环中var打印全是 3,let正常打印 0 1 2,因为let每轮循环创建新的块级作用域
二、说说 JS 中的原型和原型链
原型是什么
JS 中每个函数天生自带一个 prototype 属性(显式原型),每个对象天生自带一个 __proto__ 属性(隐式原型)。
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const p = new Person('Tom');
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null(终点)
为什么要有原型
JS 中的对象由 new 构造函数 创建。为了让所有实例共享属性和方法,就把公共方法放在 prototype 上,实例通过 __proto__ 就能访问到。
原型链
V8 查找对象属性时,沿着 __proto__ 一直往上找,直到 null 为止。这条链就是原型链。
实例 p
└── __proto__ → Person.prototype
└── __proto__ → Object.prototype
└── __proto__ → null (终点)
p.sayHi(); // 在 Person.prototype 上找到
p.toString(); // 在 Object.prototype 上找到
p.xxx; // 一路找到 null 都没有 → undefined
完整的原型链关系图
function Person() {}
// 实例 ←→ 构造函数
const p = new Person();
p.__proto__ === Person.prototype; // true
// 构造函数 ←→ Function
Person.__proto__ === Function.prototype; // true
// Function ←→ Object
Function.prototype.__proto__ === Object.prototype; // true
// Object 的终点
Object.prototype.__proto__ === null; // true
面试要点
prototype是函数的属性,__proto__是对象的属性- 原型链终点是
Object.prototype.__proto__ === null instanceof原理:沿着__proto__查找是否有=== 构造函数.prototype的节点- 不要用
obj.__proto__,推荐用Object.getPrototypeOf(obj)/Object.setPrototypeOf()
三、说一说 JS 中的继承
是什么
子类继承父类的属性和方法,避免重复定义。JS 有 5 种主要继承方式,从低到高演进。
1. 原型链继承
function Parent() {
this.colors = ['red', 'blue'];
}
Parent.prototype.say = function() { console.log('hi'); };
function Child() {}
Child.prototype = new Parent();
const c1 = new Child();
c1.colors.push('green');
const c2 = new Child();
console.log(c2.colors); // ['red', 'blue', 'green'] ← 引用类型被共享!
缺点:引用类型的属性被所有实例共享,修改一个影响其他。
2. 构造函数继承(借用调用)
function Parent() {
this.colors = ['red', 'blue'];
}
Parent.prototype.say = function() { console.log('hi'); };
function Child() {
Parent.call(this); // 借用 Parent 的 this
}
const c1 = new Child();
c1.colors.push('green');
const c2 = new Child();
console.log(c2.colors); // ['red', 'blue'] ✅ 不共享了
console.log(c1.say); // undefined ❌ 拿不到原型上的方法
缺点:只能继承实例属性,不能继承原型上的方法。
3. 组合继承(原型链 + 构造函数)
function Parent(name) {
this.name = name;
this.colors = ['red'];
}
Parent.prototype.say = function() { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name); // 第1次调用 Parent(继承实例属性)
this.age = age;
}
Child.prototype = new Parent(); // 第2次调用 Parent(继承原型)
Child.prototype.constructor = Child;
const c = new Child('Tom', 18);
c.say(); // 'Tom'
缺点:Parent 被调用了两次,浪费性能。
4. 寄生组合继承(最优解)
function Parent(name) {
this.name = name;
this.colors = ['red'];
}
Parent.prototype.say = function() { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name); // 只调1次 Parent
this.age = age;
}
// 关键:用 Object.create 继承原型,不调 Parent
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const c = new Child('Tom', 18);
c.say(); // 'Tom'
优点:Parent 只调用一次,实例属性不共享,原型方法也能继承。面试推荐写法。
5. class 继承(ES6 语法糖)
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red'];
}
say() { console.log(this.name); }
}
class Child extends Parent {
constructor(name, age) {
super(name); // 等价于 Parent.call(this, name)
this.age = age;
}
}
const c = new Child('Tom', 18);
c.say(); // 'Tom'
优点:语法清晰,extends + super 本质就是寄生组合继承。
五种方式对比
| 方式 | 实例属性共享? | 原型方法继承? | Parent 调用次数 | 推荐度 |
|---|---|---|---|---|
| 原型链继承 | 共享(有问题) | ✅ | 0 | ❌ |
| 构造函数继承 | 不共享 ✅ | ❌ | 1 | ❌ |
| 组合继承 | 不共享 ✅ | ✅ | 2 | ⚠️ |
| 寄生组合继承 | 不共享 ✅ | ✅ | 1 | ✅ |
| class 继承 | 不共享 ✅ | ✅ | 1 | ✅✅ |
面试要点
- 面试首选写寄生组合继承,核心是
Object.create(Parent.prototype)避免第二次调用 Parent Object.create的作用:创建一个新对象,其__proto__指向参数- class 继承是 ES6 语法糖,底层仍是寄生组合继承
- 别忘了修复
constructor:Child.prototype.constructor = Child
四、说说 JS 中的 this
是什么
this 是 JS 中的一个关键字,指向谁取决于函数的调用方式,而不是定义方式。
四大绑定规则 + 箭头函数
// 1. 默认绑定:独立调用,this 指向 window(严格模式 undefined)
function foo() {
console.log(this); // window
}
foo();
// 2. 隐式绑定:作为对象方法调用,this 指向该对象
const obj = {
name: 'Tom',
say() { console.log(this.name); }
};
obj.say(); // 'Tom'
// ⚠️ 隐式丢失:把方法赋值给变量后独立调用
const fn = obj.say;
fn(); // undefined(this 变成 window)
// 3. 显式绑定:call / apply / bind
function greet(greeting) {
console.log(greeting + ', ' + this.name);
}
const person = { name: 'Tom' };
greet.call(person, 'Hello'); // 'Hello, Tom'
greet.apply(person, ['Hello']); // 'Hello, Tom'
const bound = greet.bind(person, 'Hello');
bound(); // 'Hello, Tom'
// call/apply 立即执行,bind 返回新函数
// 4. new 绑定:this 指向新创建的实例
function Person(name) {
this.name = name; // this 指向新对象
}
const p = new Person('Tom');
console.log(p.name); // 'Tom'
call / apply / bind 的区别
| 方法 | 执行时机 | 参数 | 返回值 |
|---|---|---|---|
call | 立即执行 | 参数列表 (thisArg, arg1, arg2) | 函数返回值 |
apply | 立即执行 | 参数数组 (thisArg, [arg1, arg2]) | 函数返回值 |
bind | 返回新函数 | 参数列表 | 绑定后的新函数 |
箭头函数的 this
箭头函数没有自己的 this,它继承外层(定义时)的 this,且不可被 call/apply/bind 改变。
const obj = {
name: 'Tom',
// 普通函数:this 指向 obj
sayHi() {
setTimeout(function() {
console.log(this.name); // ❌ this 是 window
}, 100);
},
// 箭头函数:this 继承外层 sayHi 的 this(obj)
sayHiArrow() {
setTimeout(() => {
console.log(this.name); // ✅ 'Tom'
}, 100);
}
};
obj.sayHiArrow();
绑定优先级
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
面试要点
- this 取决于调用方式,不是定义方式
- 箭头函数没有 this,用外层的——这是 setTimeout 回调常用箭头函数的原因
- 隐式丢失:对象方法赋值给变量后独立调用,this 丢失指向 window
- call/apply/bind 区别:call/apply 立即执行,bind 返回新函数
五、说一说 JS 中的事件模型(事件流)
是什么
事件流是事件在浏览器中传播的路径,分为三个阶段:
1. 捕获阶段(Capture):从 window 往目标元素传播
2. 目标阶段(Target):到达目标元素,触发事件
3. 冒泡阶段(Bubble):从目标元素往 window 传播
<div id="outer">
<div id="inner">点击我</div>
</div>
<script>
// addEventListener 第3个参数:true=捕获阶段触发,false/默认=冒泡阶段触发
outer.addEventListener('click', () => {
console.log('outer 冒泡');
}, false);
outer.addEventListener('click', () => {
console.log('outer 捕获');
}, true);
inner.addEventListener('click', () => {
console.log('inner 目标');
}, false);
// 点击 inner,输出顺序:
// outer 捕获 → inner 目标 → outer 冒泡
</script>
阻止事件传播
element.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡/捕获,后续阶段不触发
});
element.addEventListener('click', (e) => {
e.stopImmediatePropagation(); // 阻止传播 + 阻止同元素上的其他监听器
});
| 方法 | 作用 |
|---|---|
e.stopPropagation() | 阻止事件继续传播 |
e.stopImmediatePropagation() | 阻止传播 + 阻止同元素其他监听器 |
事件委托(事件代理)
借助冒泡机制,把子元素的事件统一绑定在父元素上,减少事件绑定数量。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
<script>
// ❌ 不好的写法:每个 li 都绑定
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', () => console.log(li.textContent));
});
// ✅ 事件委托:只绑父元素
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log(e.target.textContent);
}
});
// 后续动态添加的 li 也能响应(无需重新绑定)
const newLi = document.createElement('li');
newLi.textContent = 'item 4';
list.appendChild(newLi); // 点击也能触发
</script>
面试要点
- 三个阶段:捕获(window → target)→ 目标 → 冒泡(target → window)
- 默认在冒泡阶段触发,
addEventListener第三参数传true才在捕获阶段触发 - 事件委托:利用冒泡,父元素统一处理子元素事件,动态添加的元素也能响应
stopPropagationvsstopImmediatePropagation:后者额外阻止同元素其他监听器
六、说说 JS 怎么做类型判断
五种方式总览
| 方法 | 能判断的类型 | 缺点 |
|---|---|---|
typeof | 原始类型(除 null)+ function | null 返回 'object',引用类型都是 'object' |
instanceof | 引用类型 | 不能判断原始类型,跨 iframe 失效 |
Object.prototype.toString.call() | 所有类型 | 写法长 |
Array.isArray() | 只判断数组 | 只能判断数组 |
constructor | 引用类型 | 可被修改,不靠谱 |
1. typeof
typeof 'abc'; // 'string'
typeof 123; // 'number'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Symbol(); // 'symbol'
typeof 123n; // 'bigint'
typeof null; // 'object' ⚠️ 历史遗留 bug
typeof []; // 'object' ⚠️ 分不清数组和对象
typeof {}; // 'object'
typeof function(){}; // 'function'
原理:将值转换为二进制,判断前三位是否为 000。null 的二进制全是 0,所以前三位也是 000,被误判为 object。
2. instanceof
[] instanceof Array; // true
[] instanceof Object; // true(数组也是对象)
{} instanceof Object; // true
new Date() instanceof Date; // true
/'abc' instanceof RegExp; // true
'abc' instanceof String; // false(不能判断原始类型)
原理:沿着实例的 __proto__(隐式原型链)查找,是否有 === 构造函数.prototype 的节点,找到返回 true,找到 null 返回 false。
// 手写 instanceof
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left); // left.__proto__
while (proto !== null) {
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
3. Object.prototype.toString.call()(最准确)
Object.prototype.toString.call('abc'); // '[object String]'
Object.prototype.toString.call(123); // '[object Number]'
Object.prototype.toString.call(null); // '[object Null]'
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call({}); // '[object Object]'
Object.prototype.toString.call(new Date());// '[object Date]'
Object.prototype.toString.call(/abc/); // '[object RegExp]'
// 封装一个通用类型判断函数
function getType(val) {
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
getType(null); // 'null'
getType([]); // 'array'
getType({}); // 'object'
原理:Object.prototype.toString 内部访问值的 [[Class]] 内部属性,返回 '[object ' + [[Class]] + ']'。
4. Array.isArray()
Array.isArray([]); // true
Array.isArray({}); // false
Array.isArray('abc'); // false
ES5 引入,专门用于准确判断数组,推荐判断数组时优先使用。
5. constructor
[].constructor === Array; // true
({}).constructor === Object; // true
new Date().constructor === Date; // true
// ⚠️ 不可靠:constructor 可以被修改
function Foo() {}
Foo.prototype.constructor = Object; // 被篡改
new Foo().constructor === Foo; // false
面试要点
typeof的 bug:typeof null === 'object'(历史遗留,前三位 000)- 判断数组:优先
Array.isArray(),次选Object.prototype.toString.call() - 通用类型判断:
Object.prototype.toString.call(val).slice(8, -1)最准确 instanceof原理:沿原型链查找__proto__ === 构造函数.prototypeconstructor不可靠:可以被手动修改,面试时作为"了解"即可
总结
以上 6 道题是 JS 面试的第二批核心高频考点,记忆建议:
- 作用域 → 记住三种类型 + 词法作用域(定义时确定)+
let块级作用域 - 原型链 →
prototype是函数的,__proto__是对象的,终点是null - 继承 → 面试手写寄生组合继承,
Object.create(Parent.prototype)是核心 - this → 四大规则(默认/隐式/显式/new)+ 箭头函数继承外层 this
- 事件模型 → 捕获→目标→冒泡三阶段 + 事件委托利用冒泡
- 类型判断 →
typeof判原始类型(除 null),toString.call()万能判断
配合上篇(数组/字符串方法、类型转换、== vs ===、拷贝、闭包)共 12 题,基本覆盖 JS 面试核心范围 👋