JavaScript最终考核

基础部分

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);
}

问题

  1. 上述代码输出结果是什么?为什么?

  2. 请给出两种不同方式修改代码,使其输出 0、1、2

  3. 输出结果是: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() 方法?
  1. p.proto === Person.prototype
  2. Person.prototype === p.proto
  3. p.proto .proto === Object.prototype
  4. 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
  5. 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>

深拷贝特点:

  1. 完全独立的副本
  2. 正确处理特殊对象
  3. 能递归拷贝
  4. 能处理循环引用
    实现了一个支持对象、数组、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();

常用语法:

  1. set(添加 / 修改)
    map.set(key, value);
  2. get(取值)
    map.get(key);
  3. has(判断是否存在)
    map.has(key);
  4. delete(删除)
    map.delete(key);
  5. clear(清空)
    map.clear();
  6. 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,又怕基础不牢就上手框架会走弯路。再加上前端生态更新极快,我常常一边学一边焦虑,担心自己花时间去啃的内容很快就被淘汰。但还是稳扎稳打,跟着进度学。

相关推荐
努力努力再努力wz1 小时前
【Qt入门系列】:QLabel控件详解:从文本显示到图片展示,再到内容布局与伙伴机制
android·开发语言·数据结构·数据库·c++·qt·mysql
用户4445543654261 小时前
Android跑马灯控件
前端
光影少年1 小时前
react全局状态、局部状态、服务端状态如何选型
前端·react.js·掘金·金石计划
甄心爱学习1 小时前
【项目实训(个人10)】
开发语言·前端·javascript
触底反弹1 小时前
dom操作这篇文章就够了
javascript·面试
无糖可可果1 小时前
从"查字典"到"写 Prompt":奇妙学习之旅
javascript
7yue1 小时前
我用 AI 把 Learn Claude Code 改写成了 TypeScript + 代数效应版本
前端
云宝大王1 小时前
JavaScript 异步编程:从回调到探索 Promise的秘密
前端·javascript
右耳朵猫AI1 小时前
Java & JVM技术周刊 2026年第20周
java·开发语言·jvm