基础部分
var let const的区别
var:函数作用域,存在变量提升,可以重复声明。
let:块级作用域,存在暂时性死区,不允许重复声明。
const:块级作用域,必须初始化,声明后不能重新赋值。
js
const obj = { name: "Tom" };
obj.name = "Jerry"; // 可以,修改的是对象内部属性
obj = {}; // 报错,不能修改 obj 保存的地址
结论:const 保证的是变量保存的地址不能变,不是对象内容不能变。
全局变量和全局函数与 window 对象的关系
在浏览器环境中,使用 var 声明的全局变量和 function 声明的全局函数会成为 window 对象的属性。
js
var a = 10;
function fn() {
console.log("hello");
}
console.log(window.a); // 10
window.fn(); // hello
但是 let 和 const 声明的全局变量不会挂载到 window 上。
js
let b = 20;
const c = 30;
console.log(window.b); // undefined
console.log(window.c); // undefined
null 和 undefined 的区别
undefined:表示变量声明了,但没有赋值。
null:表示人为赋值为空,通常用于表示"空对象"。
js
let a;
console.log(a); // undefined
let obj = null;
console.log(obj); // null
判断方式
js
value === undefined
value === null
value == null // 可以同时判断 null 和 undefined js规定 null == undefined
注意:typeof null 的结果是 "object",这是 JavaScript 的历史遗留问题。
this 的指向问题
普通函数中的 this 通常取决于函数的调用方式:谁调用,this 就指向谁。
js
const obj = {
name: "Tom",
say: function() {
console.log(this.name);
}
};
obj.say(); // Tom,this 指向 obj
箭头函数没有自己的 this,它会继承外层作用域中的 this。
js
const obj = {
name: "Tom",
say: () => {
console.log(this.name);
}
};
obj.say(); // 通常是 undefined,因为 this 不指向 obj
call、apply、bind 的异同
三者都可以改变函数执行时的 this 指向。
call:立即调用,参数逐个传入。
apply:立即调用,参数以数组形式传入。
bind:不会立即调用,而是返回一个新函数。
js
function fn(a, b) {
console.log(this.name, a, b);
}
const obj = { name: "Tom" };
fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);
const newFn = fn.bind(obj, 1, 2);
newFn();
原型链机制
每个函数都有 prototype 属性,每个实例对象都有 proto 属性,实例的 proto 指向构造函数的 prototype。
js
function Person(name) {
this.name = name;
}
const p = new Person("Tom");
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
关系
js
Person 构造函数
↓ prototype
Person.prototype 原型对象
↑ __proto__
p 实例对象
访问属性或方法时,会先从对象自身找,找不到就沿着原型链继续向上查找。
DOM 事件流
DOM 事件流分为三个阶段:
js
捕获阶段 → 目标阶段 → 冒泡阶段
事件委托是指把子元素的事件绑定到父元素上,利用事件冒泡统一处理。
js
const ul = document.querySelector("ul");
ul.addEventListener("click", function (e) {
if (e.target.tagName === "LI") {
console.log(e.target.innerText);
}
});
事件冒泡与捕获
事件捕获:事件从外层元素向目标元素传递。
事件冒泡:事件从目标元素向外层元素传递。
js
element.addEventListener("click", fn, true); // 捕获阶段
element.addEventListener("click", fn, false); // 冒泡阶段,默认
常见不冒泡事件:focus、blur、mouseenter、mouseleave、load、unload 等。
闭包
闭包指的是:内部函数可以访问外部函数作用域中的变量,即使外部函数已经执行结束。
优点
可以保存变量状态。
可以实现数据私有化。
缺点
可能导致内存无法及时释放。
滥用闭包会增加理解难度。
js
function createCounter() {
let count = 0;
return function () {
return count++;
};
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
特点:
函数嵌套 一个函数里返回另一个函数
访问外部变量 内部函数使用了外部变量
外部作用域未销毁 内部函数仍在被使用
防抖与节流
防抖:连续触发时,只执行最后一次。
节流:连续触发时,每隔固定时间执行一次。
防抖适合搜索框输入、按钮防重复提交;节流适合滚动加载、窗口尺寸变化、拖拽等场景。
JavaScript 是否允许隐式声明变量
非严格模式下,JavaScript 允许隐式声明变量。
js
a = 10;
console.log(window.a); // 10
这种写法会让变量变成全局变量,容易造成变量污染和难以排查的 bug。
严格模式下会直接报错。
js
"use strict";
a = 10; // ReferenceError
应用
基础部分的代码题中有一个用的方法复杂,现加以改正
字符串处理
js
function lengthOfLastWord(s) {
const arr = s.trim().split(" ");
return arr[arr.length - 1].length;
}
console.log(lengthOfLastWord("Hello World"));
// split(/\s+/) 可以按一个或多个空白字符切分
进阶部分
简答
Debug 题:作用域 + 闭包
js
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
问题
-
上述代码输出结果是什么?为什么?
-
请给出两种不同方式修改代码,使其输出 0、1、2
-
输出结果是:3 3 3
原因:var 没有块级作用域,for 循环中的 i 是同一个变量。 setTimeout 是异步任务,等回调执行时,循环已经结束,此时 i 已经变成 3。
方式一:使用let
js
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
方式二:使用立即执行函数形成闭包
js
for (var i = 0; i < 3; i++) {
(function (j) {
// 这里的箭头函数就是一个闭包,它保存了外层作用域中的j变量
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}
方式三:使用 setTimeout 第三个参数
js
// 延时函数的第三个以后的参数会传回回调函数
for (var i = 0; i < 3; i++) {
setTimeout((j) => {
console.log(j);
}, 1000, i);
}
原型链理解题
js
function Person(name) {
this.name = name;
}
Person.prototype.say = function () {
console.log(this.name);
};
const p = new Person("Tom");
问题
- p.proto === ?
- Person.prototype === ?
- p.proto .proto === ?
- 画出或描述 p、Person、Person.prototype 三者之间的关系
- 为什么 p 可以调用 say() 方法?
- p.proto === Person.prototype
- Person.prototype === p.proto
- p.proto .proto === Object.prototype
- Person 是构造函数
Person.prototype 是 Person 的原型对象
p 是通过 new Person() 创建出来的实例对象
p.proto 指向 Person.prototype
p.proto .constructor 指向 Person
Person.prototype.constructor 指向 Person
Person.prototype.proto 指向 Object.prototype
Object.prototype.proto 指向 null - p 自己身上没有 say 方法,于是会沿着原型链查找。 p.proto 指向 Person.prototype,而 say 方法正好定义在 Person.prototype 上, 所以 p 可以调用 say()。
代码
防抖 & 节流应用题
html
<!-- 场景
<input id="search" />
要求
1. 用户输入时触发请求,但需要使用防抖 500ms
2. 页面滚动加载更多数据时,使用节流 1 秒
3. 分别写出防抖和节流的实现
4. 说明防抖和节流的适用场景区别 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input id="search" placeholder="请输入搜索内容" />
<script>
// 防抖实现
const search = document.querySelector('#search');
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
function Result(e) {
console.log('发起搜索请求:', e.target.value);
}
search.addEventListener('input', debounce(Result, 500));
// 节流实现
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
// 获取时间戳
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
function loadMore() {
console.log('加载更多数据');
}
window.addEventListener('scroll', throttle(loadMore, 1000));
</script>
</body>
</html>
区别说明
防抖:连续触发时,只执行最后一次。适合搜索框输入、窗口大小变化。
节流:连续触发时,每隔固定时间执行一次。适合滚动加载、拖拽、监听页面滚动
深拷贝进阶题
html
<!-- 给定对象
const obj = {
name: "test",
date: new Date(),
reg: /abc/,
arr: [1, 2, { a: 3 }]
};
要求
- 能正确拷贝对象和数组
- 能处理 Date 和 RegExp
- 进阶:能避免循环引用导致的报错 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 为什么用 WeakMap key是对象 不会阻止垃圾回收 更适合缓存用途
function deepClone(obj, map = new WeakMap()) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (map.has(obj)) {
return map.get(obj);
}
// Date和 RegExp不是普通对象,不能 { } 或[]复制,,必须通过构造函数重新创建,这样才能保留行为和内部状态
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
const result = Array.isArray(obj) ? [] : {};
// 记录映射关系(防止循环)
map.set(obj, result);
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = deepClone(obj[key], map);
}
}
return result;
}
</script>
</body>
</html>
深拷贝特点:
- 完全独立的副本
- 正确处理特殊对象
- 能递归拷贝
- 能处理循环引用
实现了一个支持对象、数组、Date、RegExp 的深拷贝函数,并通过 WeakMap解决了循环引用问题,避免了递归栈溢出
事件委托 + 防抖 + 闭包 + this
html
<!-- HTML 结构
<ul id="list">
<li>按钮1</li>
<li>按钮2</li>
<li>按钮3</li>
</ul>
要求
- 使用事件委托,只在 ul 上绑定一次点击事件
- 点击任意 li 时,输出该按钮的文本内容
- 使用防抖 1 秒,避免频繁点击触发
- 使用闭包实现:每个按钮维护自己的点击次数
- 正确处理 this 指向,要求 this 指向被点击的元素
输出示例
按钮1 被点击,第 1 次
按钮1 被点击,第 2 次
按钮2 被点击,第 1 次 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="list">
<li>按钮1</li>
<li>按钮2</li>
<li>按钮3</li>
</ul>
<script>
const list = document.querySelector("#list");
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
// 改变this指向,固定this指向li
fn.apply(this, args);
}, delay);
};
}
function createCounterHandler() {
// 计数
const countMap = new Map();
// 每个li有自己的一个防抖函数
const debounceMap = new Map();
// 实现闭包,数据私有化
return function (target) {
// 如果没有防抖函数就创建一个
if (!debounceMap.has(target)) {
const fn = debounce(function () {
// 取出点击的li里存在的数据
const text = this.innerText;
// 记录点击次数,若不存在则为0+1
const count = (countMap.get(target) || 0) + 1;
// 将点击次数存在创建的计数map里面,每个li的计数单独拎出来
countMap.set(target, count);
console.log(`${text} 被点击,第 ${count} 次`);
}, 1000);
// 将刚才创建的防抖函数存进map里面,实现每个li有它对应的防抖函数
debounceMap.set(target, fn);
}
// 将防抖函数里面的li指向它自己,以防出错
debounceMap.get(target).call(target);
};
}
const handleClick = createCounterHandler();
list.addEventListener("click", function (e) {
if (e.target.tagName !== "LI") return;
handleClick(e.target);
});
</script>
</body>
</html>
Map使用方法
Map = 键值对集合
key 可以是 任意类型(对象、函数、DOM 都可以)
创建 Map(最基本)
const map = new Map();
常用语法:
- set(添加 / 修改)
map.set(key, value); - get(取值)
map.get(key); - has(判断是否存在)
map.has(key); - delete(删除)
map.delete(key); - clear(清空)
map.clear(); - size(长度)
map.size
reduce 综合应用题
html
<!-- 给定数据
const students = [
{ name: "Alice", grade: 10, score: 90 },
{ name: "Bob", grade: 9, score: 80 },
{ name: "Charlie", grade: 10, score: 85 },
{ name: "David", grade: 9, score: 70 },
];
要求
- 按年级分组
- 计算每个年级的平均分
- 输出指定结构
- 目标结构
{
9: { avg: 75, students: [...] },
10: { avg: 87.5, students: [...] }
} -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const students = [
{ name: "Alice", grade: 10, score: 90 },
{ name: "Bob", grade: 9, score: 80 },
{ name: "Charlie", grade: 10, score: 85 },
{ name: "David", grade: 9, score: 70 },
];
const result = students.reduce((acc, cur) => {
const grade = cur.grade;
if (!acc[grade]) {
acc[grade] = {
total: 0,
count: 0,
avg: 0,
students: []
};
}
acc[grade].students.push(cur);
acc[grade].total += cur.score;
acc[grade].count++;
acc[grade].avg = acc[grade].total / acc[grade].count;
return acc;
}, {});
for (const grade in result) {
delete result[grade].total;
delete result[grade].count;
}
console.log(result);
</script>
</body>
</html>
总结
总体感悟:"入门不难,精通极难,越学越觉得自己不懂。"
前端三件套的学习,我最大的感受是从"以为做网页就是拖控件"变成了真正理解网页是如何被一层层搭建起来的。HTML让我明白了结构语义的重要性,CSS让我第一次体会到布局和审美对用户体验的影响,而JavaScript则让我接触到真正的编程逻辑和交互思维。虽然现在能写出一些简单的页面和功能,但我很清楚自己距离"合格的前端"还很远,每一个知识点背后都能牵出一大片我不懂的东西。
与此同时,我也带着不少困惑:JS里的闭包、原型链、this指向这些概念在我脑子里还很模糊,CSS一遇到复杂布局就容易写成"补丁代码",靠不断试错堆出来效果却说不清原理;学了三件套后,我不知道是该继续死磕基础,还是可以开始接触Vue或React,又怕基础不牢就上手框架会走弯路。再加上前端生态更新极快,我常常一边学一边焦虑,担心自己花时间去啃的内容很快就被淘汰。但还是稳扎稳打,跟着进度学。