25级开学 JS 考核题解

25级开学 JS 考核题解


一、this 指向代码题

1. 讲出几大类函数this指向 - 普通函数,对象函数,构造函数,箭头函数

函数类型 this 指向 说明
普通函数(直接调用 fn() window(非严格模式)/ undefined(严格模式) 直接调用时 this 落到全局
对象函数(obj.fn() 调用它的那个对象 . 前面是谁
构造函数(new Fn() 新创建出来的实例对象 new 会把 this 绑定到新对象
箭头函数 定义时外层作用域的 this 箭头函数自己没有 this,向上找最近一层非箭头函数的 this

2. 讲出apply,call,bind的区别,以及使用方法

三者都是显式改变 this 的方法,区别在调用方式和参数:

方法 是否立即执行 传参方式
fn.call(thisArg, a, b, c) 立即执行 一个个传
fn.apply(thisArg, [a, b, c]) 立即执行 以数组传
fn.bind(thisArg, a, b) 返回新函数,不立即执行 一个个传,参数可部分预置
js 复制代码
var name = 'global'

const obj = {
  name: 'obj',
  say() {
    console.log(this.name)
    const fn = function () {
      console.log(this.name)
    }
    fn()
  }
}

obj.say()
  1. 两次 console.log 分别输出什么?

第一次 obj,第二次 global

  1. 为什么?
  • 第一次:obj.say() 是对象方法调用,this = obj,所以 this.name = obj
  • 第二次:fn() 是普通函数直接调用,this 落到全局 window。而 var name = 'global'name 挂到了 window 上,所以 this.name = global
  1. 如果把 fn 改成箭头函数:
js 复制代码
const fn = () => {
  console.log(this.name)
}

输出又是什么,为什么?

输出 obj。因为箭头函数没有自己的 this,会向上找定义它的作用域,这里定义在 say() 里,say 的 this 是 obj,所以箭头函数里的 this 也是 obj

  1. 有没有什么改变this的思路,讲讲实现bind的思路

改变 this 的思路:用 call / apply 临时借 this,或用箭头函数固定 this,或用 bind 永久绑定。

bind 的实现思路(核心):返回一个闭包,闭包里用 apply 把原函数调起来,并把 this 固定为传入的第一个参数:

js 复制代码
Function.prototype.myBind = function (context, ...args) {
  const self = this;
  return function (...innerArgs) {
    return self.apply(context, [...args, ...innerArgs]);
  };
};

二、var / let / const 全局预编译、函数预编译

1. 讲出let,const,var的区别,块级作用域,变量提升,以及后来不使用var的原因

var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 有,且提升时初始化为 undefined 有提升但进入暂时性死区 有提升但进入暂时性死区
重复声明 允许 不允许 不允许
声明内容 变量 变量 常量(必须声明时赋值,不可再赋值)
挂到 window 不会 不会

几个关键点:

  • 块级作用域:{} 内是一个作用域;函数作用域:一个函数内变量到处可见。
  • 变量提升:var 的声明会被提到函数体最前,但赋值仍在原位置,所以提升后值是 undefined
  • 暂时性死区:let/const 也有提升,但从作用域开始到 let 声明那一行之前,访问该变量会直接抛 ReferenceError
  • 全局作用域的 var 变量会挂到 window,成为 window 的一个属性。

为什么后来不用 var:

  1. 可以重复声明,容易命名冲突、覆盖。
  2. 函数作用域 + 提升,变量会"泄漏"出块、或在声明前就能读到 undefined,容易产生隐蔽 bug。
  3. 挂到 window,污染全局作用域。

2. 控制台会输出什么?

js 复制代码
var a = 1;
let b = 2;

function test() {
  console.log(a);
  console.log(b);

  var a = 3;
  let b = 4;

  console.log(a);
  console.log(b);
}

test();

console.log(a);
console.log(b);

输出:

复制代码
undefined
ReferenceError: Cannot access 'b' before initialization
  1. 进入 test(),函数内 var a 提升为 undefinedconsole.log(a)undefined
  2. console.log(b):函数内的 let b 虽然被提升,但在声明之前处于暂时性死区,访问它直接抛 ReferenceError,函数在这里中断。

3. 哪一行会报错,为什么会报错?

报错的是 console.log(b)(第五行),原因是暂时性死区:在 let b = 4 初始化之前访问 b。

外层 let b = 2 在全局作用域,函数内 let b = 4 在 test 的函数作用域,两者不是同一作用域,不算重复声明。

4. 如果把函数里面的 let b = 4 改成 var b = 4,输出又会有什么变化?

js 复制代码
function test() {
  console.log(a);   // undefined(var a 提升)
  console.log(b);   // undefined(var b 提升)
  var a = 3;
  var b = 4;
  console.log(a);   // 3
  console.log(b);   // 4
}
test();             // 依次输出:undefined, undefined, 3, 4
console.log(a);     // 1
console.log(b);     // 2

输出:undefined undefined 3 4 1 2


三、垃圾回收机制

1. 什么是垃圾回收机制?判断机制是什么?

程序会为变量自动分配内存;当某块内存不再可达时,自动回收,交给后续进程复用。JavaScript 里开发者通常不需要手动释放内存。

判断机制:标记, 清除

  • 从根对象(root)出发(全局对象 window、当前调用栈中的局部变量和参数、闭包引用的变量等),一路向下遍历所有引用,凡是能被访问到的都打上"存活"标记。
  • 遍历结束后,没有标记的对象就是垃圾,被回收。

2. 什么是内存泄漏?前端常见的内存泄漏场景有哪些?

内存泄漏:一块内存已经不再被使用了,但因为还被某个引用指着,GC 认为它"可达",一直不回收。

常见场景:

  • 全局变量:不小心把变量挂到 window,一直存活,window 是浏览器页面的全局对象。
  • 定时器没清除:setInterval 一直跑,回调里的引用无法释放。
  • 事件监听器没移除:DOM 节点被删了,但监听器还引用着它。
  • 闭包滥用:外层变量被内层函数长期持有,导致无法回收。
  • 分离的 DOM 节点:把 DOM 元素存到变量里,即使从页面移除,仍被引用。
  • 循环引用:两个对象互相引用,引用计数永远不为 0。

3. 你有了解过V8 引擎对垃圾回收机制的优化吗? 比如分代回收式垃圾回收?

V8 把堆分成新生代和老生代:

  • 新生代:新分配的对象先放这里,空间小、回收频繁,用复制算法,只保留存活对象。
  • 老生代:在新生代里熬过一定次数 GC 仍存活的对象,会晋升到老生代。空间大、回收不频繁,用标记-清除 + 标记-整理(避免碎片)。

四、闭包、作用域链

1. JS 有哪几种作用域?

  • 全局作用域(最外层)
  • 函数作用域(一个函数内)
  • 块级作用域({} 内)

2. 什么是作用域链?变量查找时按照什么顺序进行?

作用域一层套一层,形成一条由内到外的链。查找变量时,先在本作用域找,找不到就沿着作用域链往外层找,直到全局,还没找到就报 ReferenceError

3. 什么是闭包?

内层函数访问了外层函数作用域里的变量,即使外层函数执行完毕,这些变量因为仍被内层函数引用而不会被回收,这个"函数 + 被它引用的外部变量"整体就是闭包。

4. 输出什么?为什么?

js 复制代码
function outer() {
  let count = 0;
  return function () {
    count++;
    console.log(count);
  };
}

const fn1 = outer();
const fn2 = outer();

fn1();  // 1
fn1();  // 2
fn2();  // 1
fn1();  // 3
fn2();  // 2

输出:1 2 1 3 2

为什么:每次调用 outer() 都会新创建一个 countfn1fn2 各自闭包住自己那一份 count,互不影响。所以 fn1 的 count 是 0 → 1 → 2 → 3,fn2 的 count 是 0 → 1 → 2。


五、原型与原型链

js 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype.name = "prototype";
Person.prototype.age = 18;

const p = new Person("Jason");

console.log(p.name);                            
console.log(p.age);                             
console.log(p.hasOwnProperty("name"));          
console.log(p.hasOwnProperty("age"));           
console.log(p.__proto__ === Person.prototype);  
console.log(Person.prototype.constructor === Person); 

p.age = 20;

console.log(p.age);                             
console.log(Person.prototype.age);              

delete p.age;

console.log(p.age);                             

1. 九次console.log分别输出什么?

输出 原因
"Jason" p 自己的 name 属性(实例属性)
18 自己没 age,沿原型链到 Person.prototype.age
true name 是实例自有属性
false age 是继承来的,不是自有属性
true 实例的原型就是构造函数的 prototype
true prototype.constructor 指向构造函数本身
20 p.age 赋值,在 p 自己身上新增了 age = 20
18 原型上的 age 没被改动
18 删掉 p 自己的 age,又沿原型链找到 18

2. p.name最开始在哪里获取?p.__proto__指向什么?

  • p.name 最先看 p 对象自己身上有没有 name,有就直接用(实例属性 "Jason"),不会再去原型找。
  • p.__proto__ 指向 Person.prototype

3. 给 p.age 赋值后,为什么Person.prototype.age没有被修改?删除 p.age 后,为什么仍然能访问到 18

  • p.age = 20 是给实例 p 自己新增一个 age 属性(或改自己身上的),它只是遮蔽了原型上的 age,并不会去修改 Person.prototype.age
  • delete p.age 删掉 p 自己的 age 后,再次访问 p.age 就沿 __proto__ 向上,在 Person.prototype 上找到 age = 18

4. 什么是原型链?试着讲讲实例p的完整原型链以及原型链的末尾是什么。

原型链:对象通过内部指针 __proto__ 一层层向上找属性和方法,直到 null

实例 p 的完整原型链:

复制代码
p.__proto__-->  Person.prototype.__proto__-->  Object.prototype.__proto__-->  null

原型链的末尾是 nullObject.prototype.__proto__ === null)。


六、深浅拷贝

1. 讲讲你理解的深浅拷贝,并说出两个拷贝对象的浅拷贝方法

  • 浅拷贝:只复制第一层;嵌套的对象/数组仍共享同一个引用,改里层的会互相影响。
  • 深拷贝:递归复制所有层级,得到一个完全独立的新对象。

浅拷贝方法:

  • 方法一:Object.assign({}, obj),把 obj 的可枚举自有属性复制到空对象。
  • 方法二:展开运算符 { ...obj }(数组用 [...arr]arr.slice()arr.concat())。

2. 什么是循环引用?如果你想自己实现深拷贝,讲讲循环引用可能导致的问题以及解决方法

循环引用:对象内部直接或间接引用回自己(如 obj.self = obj,或 A 引用 B、B 又引用 A)。

问题:如果手写深拷贝只是无脑递归,遇到循环引用会无限递归 → 栈溢出(Stack Overflow)。

解决:用一个 Map/WeakMap 记录"已经拷贝过的对象",遇到重复引用时直接返回之前缓存的拷贝:

js 复制代码
function deepClone(obj, map = new Map()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj);     // 已拷贝过,直接返回,打破循环
  const copy = Array.isArray(obj) ? [] : {};
  map.set(obj, copy);
  for (const key of Object.keys(obj)) {
    copy[key] = deepClone(obj[key], map);
  }
  return copy;
}

3. 使用 JSON.parse(JSON.stringify(obj)) 实现深拷贝有什么局限?它无法正确处理什么数据(至少两种)?给出一种更适合的深拷贝原生方法。

局限:

  • undefined、函数、Symbol:作为值会被丢掉(对象的这些属性会被忽略,数组里变成 null)。
  • NaNInfinity → 变成 null
  • Date → 变成字符串。
  • RegExpMapSet → 变成 {}
  • 循环引用 → 直接抛错。
  • 丢失原型链、BigInt 直接报错。

更好的原生方法:structuredClone(obj),能处理循环引用、Date、Map/Set、ArrayBuffer 等大多数类型(仍不支持函数、Symbol)。


七、隐式转换

1. 什么时候会出现隐式转换?请说说不同运算符的转换规则是什么。( + - * / ==)

出现时机:参与运算的两个值类型不同,或在布尔/字符串/数字语境下需要类型统一时。

运算符 转换规则
+ 只要有一边是字符串,就做字符串拼接(另一边转字符串);否则两边都转数字相加
- * / 两边都转成 Number 再运算
== 宽松相等,按类型对齐规则转换

补充 == 的常见规则:对象 和 数字/布尔 → 对象先 ToPrimitive 再比较;布尔 和 其他 → 布尔先转数字;null == undefined 为 true。

2. \[\] == false 的输出是什么,并判断过程

输出:true

过程:

  1. false 是布尔,== 比较时布尔先转数字:false → 0
  2. [] 是对象,转原始值 ToPrimitive:先 valueOf() 得到 [](还是对象),再 toString() 得到 ""
  3. "" 转数字 → 0
  4. 0 == 0true

八、数据类型

1. js有几种数据类型?分别是什么?指出哪些是原始类型,哪些是引用类型?SymbolBigInt 的作用?

  • 原始类型(7 种):StringNumberBooleannullundefinedSymbolBigInt
  • 引用类型:Object

Symbol 的作用:生成独一无二的值,可用作对象属性名,避免属性名冲突、防止被意外覆盖。

BigInt 的作用:表示 Number 无法精确表示的大整数(超过 2^53 - 1,即 Number.MAX_SAFE_INTEGER)。

2. 获取数据类型方式有哪些?各自的特点

方式 特点
typeof x 返回 "string"/"number"/"boolean"/"undefined"/"object"/"function"/"symbol"/"bigint"null 错报为 "object",数组也是 "object"
x instanceof Ctor 判断某对象是否在某构造函数的原型链上;对原始类型为 false,跨 iframe 会失效
Object.prototype.toString.call(x) 最准确,返回 "[object Array]""[object Null]"
Array.isArray(x) 专门判断数组,最可靠

3. 为什么 typeof null === "object"

这是历史遗留 bug。早期 JS 用低位二进制表示类型标签,对象的类型标签是 000,而 null 被表示为全 0(机器码 0x00),恰好和对象标签一致,于是 typeof null 就返回了 "object"。这个 bug 一直保留下来以兼容旧代码。


九、任务队列

JS中哪些任务是同步任务 宏任务 微任务?

  • 同步任务:主线程直接按顺序执行的代码。
  • 宏任务:script 整体、setTimeoutsetInterval、I/O、网络请求、事件回调(如 click)、requestAnimationFrame 等。
  • 微任务:Promise.then/catch/finallyawait 之后的代码、queueMicrotask(手动把一个函数放进微任务队列)、MutationObserver(监听 dom 的变化) 等。

每执行完一个宏任务,就要清空整个微任务队列,再执行下一个宏任务。

下列代码的控制台输出是什么?

js 复制代码
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 1'));
  console.log('Listener 1');
});
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 2'));
  console.log('Listener 2');
});

输出:

复制代码
Listener 1
Listener 2
Microtask 1
Microtask 2

原因:一次点击只触发一个 click 事件,两个监听器在同一次事件分发(同一个宏任务)里按顺序同步执行:

  1. 监听器 1:先把 Microtask 1 放进微任务队列,再打印 Listener 1
  2. 监听器 2:把 Microtask 2 放进微任务队列,再打印 Listener 2
  3. 整个 click 宏任务结束 → 清空微任务队列,依次执行 Microtask 1Microtask 2
  4. 注意一次点击算一个宏任务,而不是一个事件监听,一次点击的宏任务会把所有绑定的事件按顺序执行

十、异步编程

JS运行特点?为什么需要异步编程?什么是回调地狱?如何解决回调地狱?

JS 运行特点:单线程 + 事件循环。主线程一次只能做一件事,但遇到 I/O、定时器、网络等耗时操作时不会傻等,而是交给浏览器/环境处理,回调在合适时机再回到主线程执行。

为什么需要异步:避免耗时操作阻塞主线程(页面卡死),提高响应速度和吞吐。

回调地狱:多层回调嵌套(fn1(() => fn2(() => fn3(...)))),代码缩进越来越深、难以阅读和维护。

如何解决:

  • 用 Promise:把回调变成链式 .then(),避免深层嵌套。
  • 用 async / await:本质还是异步,但写起来像同步,代码从上往下读,最直观。
js 复制代码
async function task() {
  const a = await fetchData();
  const b = await fetchMore(a);
  return b;
}

十一、ES6 新特性

js 复制代码
const id = Symbol("id");

const user = {
  name: "Tom",
  age: 18,
  [id]: 1001
};

function fn(user) {
  const { name, age } = user;
  console.log(arguments.length);   
  console.log(name, age);          
  console.log(user.id);            
  console.log(user[id]);           
}

fn(user, "extra");

1. 四次 console.log 分别输出什么?

输出 原因
2 fn 被调用时传了 2 个实参(user"extra"
Tom 18 解构出来的 nameage
undefined user.id 访问的是字符串属性名 "id",对象里没有这个属性
1001 user[id] 用变量 id(Symbol)作键,命中了 [id]: 1001

2. 为什么 arguments.length 是 2?

arguments 是函数内的类数组对象,保存实际传入的所有实参。这里 fn(user, "extra") 传了两个参数,所以 length === 2(与形参个数无关,只看调用时传了几个)。

3. const { name, age } = user 是什么语法?

对象解构赋值。把 user 对象里的 nameage 两个属性分别提取到同名变量 nameage 中。

4. user[id]user.id 有什么区别?

  • user.id:访问字符串键 "id" 对应的属性,对象里没有 → undefined
  • user[id]:用变量 id(其值是 Symbol("id"))作键访问,对象定义时用 [id]: 1001 存的就是这个 Symbol 键 → 1001

user.id 相当于 user["id"],和 user[Symbol("id")] 是完全不同的两个键。

5. Symbol 作为对象属性名有什么作用?

  • 生成独一无二的键,避免和其他属性名冲突。
  • 无法被 for...inObject.keys() 枚举到(需要用 Object.getOwnPropertySymbols() 才能拿到),起到一定的"隐藏/保护"作用。
  • 防止外部代码用字符串拼出同名键意外覆盖它。
相关推荐
meilindehuzi_a1 小时前
LangChain.js + Milvus 向量长期记忆实战:对话写入、语义检索与 RAG 增强
javascript·langchain·milvus
leoZ2311 小时前
2026-09-08-mysql-57-init-walkthrough
前端·javascript·数据库·vue.js·opencv·mysql·adb
艾伦野鸽ggg1 小时前
JavaScript 原型链与原型对象详解
javascript·原型模式
paopaokaka_luck1 小时前
非遗文物数字化系统(AI非遗问答、ONNX图像识别、协同过滤推荐、ECharts数据分析、非遗知识浏览与互动、文创商城订单闭环、文化活动报名签到、社区交流)
前端·javascript·vue.js·人工智能·数据分析·echarts
乘风gg2 小时前
AI Coding 提效 2 倍是真的吗?到底怎么衡量效果
前端·ai编程·claude
用户921080262862 小时前
从对象到原型链:理解 this、构造函数和 new
前端
猫不易2 小时前
Webpack 与 Vite:从 Loader / Plugin 到 Rolldown 统一引擎
前端·vite
YHL2 小时前
🎯 Danci —— 用 AI 驱动开发一个全栈英语单词学习平台
前端·后端
平头哥~2 小时前
Day 20 _ 3D 翻卡_perspective 写错人,卡片就穿帮
前端·3d·css3·学习资料