阅读视图

发现新文章,点击刷新页面。

CSS-HTML Form 表单交互深度指南

前言

虽然现代前端框架(Vue/React)已经极大简化了表单处理,但理解原生 Form 表单 的事件流、控件属性和 API,依然是处理复杂业务逻辑(如埋点、自定义验证、无刷新提交)的基础。

一、 Form 表单的核心机制

<form> 是所有输入控件的容器。它不仅负责数据的收集,还管理着数据的提交 (Submit)重置 (Reset) 周期。

1. 关键属性

  • action:数据提交的目标 URL。
  • method:HTTP 请求方式(GET 拼接到 URL,POST 放入请求体)。

2. 提交与拦截

当表单内存在 type="submit" 的按钮时,点击会触发 submit 事件。

const form = document.querySelector("#myForm");

form.addEventListener("submit", (event) => {
  // 1. 阻止浏览器默认的跳转刷新行为
  event.preventDefault(); 
  
  // 2. 自定义验证逻辑
  if (inputValue === "") {
    alert("内容不能为空");
    return;
  }
  
  // 3. 执行异步提交(如使用 Fetch/Axios)
  console.log("表单已提交");
});

3. 重置行为

form.reset() 不仅仅是清空。它会将所有字段恢复为页面加载时的初始值(例如 <input value="default"> 会恢复为 "default" 而非空)。


二、 输入控件的“通用武器库”

无论 inputselect 还是 textarea,它们都共享以下核心属性和方法:

1. 公共属性与方法

  • disabled:禁用控件,数据不会被提交。
  • readOnly:只读,数据随表单提交。
  • form:只读属性,返回当前控件所属的表单对象引用。
  • focus() / blur() :手动控制焦点的获取与失去。

2. 三大核心事件

事件 触发时机
focus 控件获得焦点时。
blur 控件失去焦点时(常用于输入后的实时验证)。
change 内容改变且失去焦点时触发(注意:与 input 事件实时触发不同)。

三、 文本输入:Input vs Textarea

1. 单行文本框 <input type="text">

  • placeholder:提示文本。
  • maxlength:硬性限制用户输入的字符长度。

2. 多行文本框 <textarea>

  • rows/cols:控制显示的行数和宽度。

  • wrap 换行控制

    • soft(默认):提交时不带换行符。
    • hard:提交的数据中包含换行符(必须配合 cols 使用)。

四、 选择框:Select 与 Option

<select> 元素在 JavaScript 中拥有更丰富的集合操作。

1. Select 关键操作

  • multiple:是否允许多选(按住 Ctrl/Command 键)。
  • options:返回包含所有 <option> 元素的 HTMLCollection。
  • remove(index) :移除指定索引的选项。

2. Option 选项详情

每一个 Option 对象都有:

  • index:在下拉列表中的位置。
  • selected:布尔值,通过设置为 true 可实现代码控制选中。
  • text:用户看到的文字。
  • value:提交给后端的值。

五、 面试模拟题

Q1:submit 事件和按钮的 click 事件有什么区别?

参考回答:

submit 事件是绑定在 form 元素上的。如果用户在输入框中按“回车键”,或者点击了 type="submit" 的按钮,都会触发 form 的 submit 事件。相比点击事件,监听 submit 能更全面地捕获提交动作。

Q2:如何通过原生 JS 获取表单内的所有数据?

参考回答:

现代浏览器推荐使用 FormData 对象:

const formData = new FormData(formElement);
// 获取特定字段
const username = formData.get('username');
// 转化为对象
const data = Object.fromEntries(formData.entries());

Q3:disabledreadOnly 在表单提交时有什么区别?

参考回答:

  • 设置了 disabled 的控件,其值在表单提交时会被忽略,且用户无法交互。
  • 设置了 readOnly 的控件,用户无法修改值,但其值在提交时会被包含在表单数据中。

掌握 CSS 布局基石:行内、块级、行内块元素深度解析

前言

在 CSS 世界中,每个元素都有一个默认的 display 属性。理解这些元素的显示模式,是解决“为什么我的宽高设置无效?”、“为什么两个 div 不在一行?”等问题的关键。

一、 三大元素显示模式对比

1. 块级元素 (Block Elements)

块级元素就像是积木,默认从上往下堆叠。

  • 特点

    • 独占一行:默认占满父容器 100% 宽度。
    • 属性全开:支持设置 widthheightmarginpadding
    • 嵌套规则:可以包含行内元素和其他块级元素(注意:ph1~h6 比较特殊,建议不要包裹块级元素)。
  • 代表标签div, p, h1~h6, ul, ol, li, header, footer, section 等。

2. 行内元素 (Inline Elements)

行内元素就像是文本,随内容流动。

  • 特点

    • 并排显示:相邻元素在同一行内排列,直到排不下才换行。
    • 宽高无效:设置 widthheight 不起作用,宽度由内容撑开。
    • 间距局限:水平方向的 marginpadding 有效;垂直方向无效(不占据空间,但可能背景会溢出)。
  • 代表标签span, a, strong, em, i, label

3. 行内块元素 (Inline-Block)

结合了前两者的优点,既能并排显示,又能设置宽高。

  • 特点

    • 并排排列:不独占一行。
    • 属性支持:支持设置 widthheightmarginpadding
  • 代表标签img, input, button, textarea, select

    :这些元素在 CSS 规范中被称为“可替换元素”,它们天生具有行内块的特性。


二、 inline-block 的“间隙之谜”

1. 产生原因

当你给子元素设置 display: inline-block 时,HTML 代码中标签之间的空格或换行符会被浏览器解析为一个约 4px 的空白字符。

2. 解决方案

  • 方法 A:父元素设置 font-size: 0(最常用)

    .parent { font-size: 0; }
    .child { display: inline-block; font-size: 14px; } /* 子元素需手动恢复字号 */
    
  • 方法 B:标签首尾相接(代码极丑,不推荐)

    <div class="child">A</div><div class="child">B</div>
    
  • 方法 C:改用 Flex 布局(现代开发首选)

    .parent { display: flex; } /* 彻底告别间隙问题 */
    

三、 空元素 (Void Elements)

空元素是指没有子节点且没有结束标签的元素,它们通常通过属性来承载内容。

  • 常见标签<br>, <hr>, <img>, <input>, <link>, <meta>

四、 面试模拟题

Q1:如何让行内元素(如 span)支持宽高?

参考回答:

  1. 修改 display 属性为 blockinline-block
  2. 设置 float(浮动后的元素会自动变为块级表现)。
  3. 设置 position: absolutefixed

Q2:img 标签是行内元素还是块级元素?为什么它可以设置宽高?

参考回答: img 在表现上属于行内元素(不换行),但它是一个可替换元素(Replaced element) 。可替换元素的内容不受 CSS 控制,其外观由标签属性决定。浏览器在渲染这类元素时,会赋予它们类似 inline-block 的特性,因此可以设置宽高。

Q3:display: nonevisibility: hidden 有什么区别?

参考回答:

  • display: none:脱离文档流,不占据空间,会引起回流(Reflow)。
  • visibility: hidden:隐藏内容,但保留占据的物理空间,不会引起回流,仅引起重绘(Repaint)。

CSS 核心基石-彻底搞懂“盒子模型”与“外边距合并”

前言

在网页布局中,万物皆“盒子”。理解盒子模型的构造及其不同模式的差异,是实现精准布局的前提。本文将从基础构成到进阶属性,带你全方位梳理 CSS 盒子模型。

一、 盒子的基本构成

一个完整的 CSS 盒子由内到外由以下四个部分组成:

  1. Content(内容) :存放文本或图片的区域。
  2. Padding(内边距) :内容与边框之间的透明区域。
  3. Border(边框) :包裹在内边距和内容外的线。
  4. Margin(外边距) :盒子与其他元素之间的距离。

1. Margin 的简写规则(顺时针原则)

  • 1 个值all (四周)
  • 2 个值top/bottom , left/right
  • 3 个值top , left/right , bottom
  • 4 个值top , right , bottom , left (上右下左,顺时针)

2. Border 的复合属性

语法:border: width style color; 例如:border: 2px solid #333;


二、 两大盒模型:标准 vs IE

通过 box-sizing 属性,我们可以切换盒子的计算方式。这是开发中处理“明明设置了宽度,盒子却被撑大”问题的关键。

1. 标准盒模型 (content-box)

  • 默认值
  • 计算公式实际宽度 = width

2. IE 盒模型 / 怪异盒模型 (border-box)

  • 推荐使用
  • 计算公式实际宽度 = width + padding + border
  • 优势:设定好的宽度不会被 padding 撑开,更符合人的直觉。
/* 全局推荐方案 */
* {
  box-sizing: border-box;
}

三、 外边距合并与合并高度

普通文档流中,两个垂直相邻的块级元素,其 margin-topmargin-bottom 会发生折叠(Collapse)。

1. 合并规则

  • 同号:取两者中的较大值
  • 异号:取两者相加之和

2. 经典面试:如何防止外边距合并?

这通常涉及触发 BFC(块级格式化上下文)

  • 为元素设置 display: inline-block
  • 设置 overflow 不为 visible(如 hidden)。
  • 使用 flexgrid 布局(它们内部的子元素不会发生 margin 合并)。

四、 现代布局新特性

1. aspect-ratio(宽高比)

现在只需要一个属性即可设置元素宽高比:

.video-card {
  width: 100%;
  aspect-ratio: 16 / 9; /* 自动根据宽度计算高度 */
  background: #000;
}

五、 面试模拟题

Q1:为什么设置 width: 100% 后再加 padding 页面会出现滚动条?如何解决?

参考回答: 因为默认是标准盒模型 (content-box),100% 宽度加上 padding 后的总宽度超过了父容器。 解决方案:将该元素的 box-sizing 设置为 border-box

Q2:什么是 BFC?它与盒模型有什么关系?

参考回答: BFC 是页面上的一个独立渲染区域。在 BFC 内部,盒子的布局不会影响到外部。利用 BFC 可以:

  1. 防止垂直外边距合并。
  2. 清除内部浮动(父元素高度塌陷问题)。
  3. 防止元素被浮动元素遮盖。

Q3:margin: auto 为什么能实现水平居中?

参考回答: 在块级元素设定了固定 width 的情况下,将左右 margin 设置为 auto,浏览器会自动平分剩余的可用空间,从而使元素居中。注意:垂直方向的 margin: auto 只有在 Flex 布局或绝对定位下才有效。

JS-前端埋点神器 navigator.sendBeacon 全指南

前言

在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户关闭页面刷新跳转路由时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。

但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— navigator.sendBeacon

一、 痛点与传统方案的挣扎

场景还原

当用户点击关闭按钮时,浏览器会触发生命周期事件(unloadvisibilitychange)。如果我们直接使用普通的异步 AJAX (xhrfetch) 发送请求,浏览器通常会忽略它,因为页面都要销毁了,浏览器不想处理未完成的请求。

传统方案:同步 XHR

为了保证数据能发出去,以前的做法是将请求改为同步(Synchronous)

const syncReport = (url, { data = {}, headers = {} } = {}) => {
  const xhr = new XMLHttpRequest();
  // 第三个参数 false 表示同步请求
  xhr.open('POST', url, false);
  xhr.withCredentials = true;
  Object.keys(headers).forEach((key) => {
    xhr.setRequestHeader(key, headers[key]);
  });
  xhr.send(JSON.stringify(data));
};

致命缺陷

  1. 用户体验极差:同步请求会阻塞主线程。这意味着只有请求发送完成,页面才能关闭或跳转。在弱网环境下,用户会感觉页面“卡死”了。
  2. 浏览器废弃:现代浏览器(如 Chrome)已经明确表示将在页面卸载期间禁用同步 XHR,这种方法迟早失效。

二、 救世主:navigator.sendBeacon

1. 概念

navigator.sendBeacon() 是专门为“页面卸载时发送数据”而设计的 Web API。 它的核心能力是:将数据放入浏览器的发送队列,即使页面已经关闭,浏览器也会在后台默默完成发送。

2. 核心优势

  • 可靠性高:不受页面生命周期影响,确保数据不丢失。
  • 非阻塞:完全异步执行,不会阻塞页面关闭或跳转,用户体验丝滑。
  • 低优先级:浏览器会择机发送(通常是网络空闲时),不争抢关键资源。

3. API 语法

const result = navigator.sendBeacon(url, data);
  • url:请求地址。

  • data:要发送的数据,支持 ArrayBufferArrayBufferViewBlobDOMStringFormDataURLSearchParams

  • result(返回值):布尔值 (true / false)。

    • true:表示数据成功加入传输队列(注意:不代表服务器接收成功)。
    • false:表示队列已满,无法加入。

三、 实战:三种常见发送姿势

1. 发送普通字符串

默认 Content-Typetext/plain

const reportData = (url, data) => {
  // data 可能会被转为字符串 "[object Object]",建议先 stringify
  navigator.sendBeacon(url, JSON.stringify(data));
};

2. 发送 JSON 数据(推荐)

如果你希望后端接收到的 Content-Typeapplication/json 或者 application/x-www-form-urlencoded,需要使用 Blob 来手动指定。

const reportData = (url, data) => {
  // ✅ 正确写法:Blob 的第二个参数才是 options
  const blob = new Blob([JSON.stringify(data)], {
    type: 'application/json; charset=UTF-8' // 或者 application/x-www-form-urlencoded
  });
  navigator.sendBeacon(url, blob);
};

3. 发送 FormData

适用于需要上传文件或模拟表单提交的场景。浏览器会自动设置 Content-Typemultipart/form-data

const reportData = (url, data) => {
  const formData = new FormData();
  Object.keys(data).forEach((key) => {
    let value = data[key];
    // FormData 的 value 只能是字符串或 Blob
    if (typeof value !== 'string' && !(value instanceof Blob)) {
      value = JSON.stringify(value);
    }
    formData.append(key, value);
  });
  navigator.sendBeacon(url, formData);
};

四、 避坑指南(面试考点)

  1. 请求类型固定sendBeacon 只能发送 POST 请求。
  2. 无法读取响应:这是一个“射后不理”的 API,你无法获取服务器返回的数据(状态码、Response Body 等)。
  3. 数据大小限制:虽然标准没有明确规定,但浏览器对队列总大小有限制(通常在 64KB 左右),不适合发送大数据。
  4. Cookie 携带sendBeacon 默认会携带同域的 Cookie。

五、 面试模拟题

Q1:sendBeaconajax (XHR/Fetch) 有什么根本区别?

参考回答:

  • 生命周期:Ajax 请求属于页面上下文,页面关闭时请求会被取消(除非同步);sendBeacon 属于浏览器上下文,页面关闭后依然存活。
  • 交互体验:页面卸载时,同步 Ajax 会阻塞跳转;sendBeacon 是异步非阻塞的。
  • 功能限制sendBeacon 只能 POST,无法自定义 headers(除了 Content-Type),且无法读取响应。

Q2:如果浏览器不支持 sendBeacon 怎么办?

参考回答: 需要做降级处理。

  1. 检测 navigator.sendBeacon 是否存在。
  2. 如果不存在,降级为 同步 XHR 请求(虽然体验差,但得保数据)。
  3. 或者使用 <img> 标签发送 GET 请求(仅限数据量极小且不需要响应的场景)。

Q3:sendBeacon 返回 true 代表数据一定发送成功了吗?

参考回答: 不一定。返回 true 仅代表浏览器成功将数据加入了发送队列。如果网络断开、或者浏览器崩溃,数据依然可能发送失败。但相比于普通 Ajax,它的成功率已经高出了几个数量级。

JS-Navigator 对象全方位实战指南

前言

在前端开发中,我们需要了解用户到底在用什么设备、网络状况如何、甚至物理位置在哪里。这时,navigator 对象就是我们派出的“侦探”。它存储了浏览器的版本、操作系统、设备能力等关键信息。今天我们就来盘点 navigator 中那些高频使用的核心技能。

一、 核心身份识别:UserAgent

navigator.userAgent 是实战中用来判断设备类型(iOS/Android/PC)的基石。

// 简单的设备判断函数
const isMobile = /Mobi|Android|iPhone/i.test(navigator.userAgent);
console.log(isMobile ? "当前是移动端" : "当前是 PC 端");

注意:随着隐私保护加强,现代浏览器正在推广 User-Agent Client Hints (navigator.userAgentData) 来逐步替代 userAgent


二、 现代 API 实战(高频场景)

navigator 不仅仅是用来读属性的,它还挂载了许多强大的 API。

1. 剪切板操作:Clipboard API

早期的 document.execCommand 已被废弃,现代复制粘贴使用 navigator.clipboard,它是异步的。

// 复制文本到剪切板
async function copyText(text) {
  try {
    await navigator.clipboard.writeText(text);
    console.log('复制成功!');
  } catch (err) {
    console.error('复制失败: ', err);
  }
}

// 读取剪切板内容
async function readText() {
  const text = await navigator.clipboard.readText(); 
  console.log('剪切板内容:', text); // text为复制到剪切板上内容
}

2. 页面卸载时的“遗言”:sendBeacon

面试必考点:如何在页面关闭(unload)前可靠地发送埋点数据?

使用 XHR 或 Fetch 可能会因为页面关闭而被浏览器取消,使用同步 AJAX 会阻塞页面关闭影响体验。sendBeacon 是最佳解。

  • 特点:异步发送、不阻塞页面关闭、但是不一定保证发送成功!!!
// 页面卸载时发送数据
window.addEventListener('unload', function() {
  const data = JSON.stringify({ event: 'page_close', time: Date.now() });
  
  // 注意:如果后端需要 JSON 格式,建议使用 Blob 设置 Header
  const blob = new Blob([data], { type: 'application/json' });
  
  const result = navigator.sendBeacon('/api/log', blob);
  console.log(result ? "埋点进入发送队列" : "发送队列已满");
});

3. 地理位置:Geolocation

获取用户经纬度,常用于地图或本地服务。

  • 注意:必须在 HTTPS 环境下才能调用,且需要用户授权。
if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition(
    (pos) => {
      console.log(`维度: ${pos.coords.latitude}`); 
      console.log(`经度: ${pos.coords.longitude}`);
    },
    (err) => {
      console.error("定位失败(可能是用户拒绝或超时):", err.message);
    },
    { timeout: 5000 } // 设置超时时间
  );
}

三、 环境嗅探属性

属性 描述 示例值
language 浏览器首选语言 "zh-CN"
cookieEnabled 是否启用 Cookie true
platform 操作系统平台(已废弃但常用) "Win32", "MacIntel"
hardwareConcurrency CPU 逻辑核心数 8 (常用于决定开启多少 Web Worker)

四、 网络状态的“假象”:onLine

navigator.onLine 返回 true 表示设备连接到了局域网或路由器,并不代表一定能访问互联网(比如连了 wifi 但宽带欠费了)。

因此,更严谨的网络检测通常结合 window 的事件监听:

function updateStatus() {
  const status = document.getElementById('status');
  if (navigator.onLine) {
    console.log("网络已连接(但不一定能上网)");
    // 实际场景中,这里通常会发一个请求 ping 一下服务器来确认真连网
  } else {
    console.log("网络已断开");
  }
}

// 监听网络变化事件
window.addEventListener('online', updateStatus);
window.addEventListener('offline', updateStatus);

五、 面试模拟题

Q1:如何判断当前用户是否处于断网状态?

参考回答:

初步判断可以使用 navigator.onLine 属性,配合 window 的 online 和 offline 事件监听。但 navigator.onLine 存在误报(只检测网卡连接状态),最稳妥的方式是配合一个轻量级的 Ajax 请求(Ping)或者加载一张 1x1 像素的图片来检测实际连通性。

Q2:navigator.sendBeacon 和普通 AJAX 请求有什么区别?

参考回答:

  1. 优先级sendBeacon 是为了解决页面卸载时发送数据的问题设计的,浏览器会将其放入专门的队列,即使页面已关闭,浏览器也会在后台完成发送。
  2. 不阻塞:它完全异步,不会像同步 XHR 那样阻塞页面跳转。
  3. 请求类型:只能发送 POST 请求,且无法读取服务器的响应内容(它是“射后不理”的)。

Q3:如何获取用户的剪切板内容?有什么限制?

参考回答:

使用 navigator.clipboard.readText()。

限制:

  1. 必须在 HTTPS 环境下使用。
  2. 必须由 用户交互(如点击事件)触发,不能自动读取。
  3. 浏览器通常会弹窗询问用户是否允许读取。

JS-深度解密 History API:单页应用(SPA)实现无刷新跳转的底层逻辑

前言

在现代前端开发中,我们习惯了页面不刷新但 URL 却在变化的体验。这背后除了 Hash 模式,最核心的功臣就是 History 对象。它不仅能控制页面的前进后退,还能在不触发请求的情况下修改地址栏。

一、 History 对象:用户的“航迹云”

window.history 存储了用户在当前窗口中访问的所有记录。为了隐私保护,你无法看到具体的 URL 列表,但可以通过它自由穿梭。

1. 基础属性

  • length:历史堆栈中的条目总数(包括当前页)。
  • state:返回当前历史条目的状态对象副本。

2. 基础导航方法

  • back() :后退一页。

  • forward() :前进一页。

  • go(n)

    • n > 0:前进 n 步。
    • n < 0:后退 n 步。
    • n = 0 或不传:刷新当前页面

二、 HTML5 状态管理:无刷新跳转的核心

HTML5 为 history 引入了两个重量级方法:pushStatereplaceState。它们允许我们在不请求服务器的情况下,手动修改浏览器的地址栏。

1. pushState(state, title, url) —— 新增记录

  • 作用:在历史记录栈中添加一个新条目。

  • 参数

    • state:一个 JSON 对象,用于存储自定义信息。
    • title:目前大多数浏览器忽略,传 "" 即可。
    • url:新的 URL 地址,必须与当前页面同源

2. replaceState(state, title, url) —— 替换记录

  • 作用:修改当前的历史记录,而不是创建新的。
  • 影响:调用后 history.length 不会增加。

⚠️ 核心特点(避坑指南):

  • 不刷新:调用这两个方法后,地址栏变了,但浏览器不会检查 URL 是否存在,也不会重新加载页面。
  • 不触发 popstate:手动调用 pushStatereplaceState 不会触发 popstate 事件。

三、 popstate 事件:监听历史变动

虽然 pushState 不会触发 popstate,但浏览器的原生行为会触发它:

  • 点击“后退”或“前进”按钮。
  • 在 JS 中调用 back()forward()go()
window.addEventListener('popstate', (event) => {
    console.log("检测到路径变化,当前关联的状态数据:", event.state);
    // 这里通常是单页路由的核心:根据新的状态更新 UI 组件
});

四、 实战:为什么 Vue/React Router 需要它?

在 History 模式下,路由系统会拦截链接的默认点击行为,改为调用 history.pushState

  1. 用户点击链接 ➜ 拦截默认跳转 ➜ 执行 pushState('/new-path')
  2. 地址栏更新 ➜ 页面不刷新。
  3. 代码主动更新组件 ➜ 渲染新内容。
  4. 用户点击后退 ➜ 触发 popstate ➜ 路由监听到变化 ➜ 切换回旧组件。

五、 面试模拟题

Q1:history.pushStatelocation.href 有什么区别?

参考回答:

  • location.href 会导致浏览器立即向服务器发送请求,触发整页刷新。
  • history.pushState 仅修改浏览器地址栏并增加历史记录,不会触发网络请求,也不会刷新页面,是单页应用无刷新跳转的基础。

Q2:为什么 History 模式在刷新页面时会出现 404?如何解决?

参考回答: 因为 pushState 设置的 URL 是前端虚拟的。用户直接刷新时,浏览器会向服务器请求这个不存在的物理路径。 解决方法:需要后端(Nginx/Node)配合。将所有未命中的静态资源请求统一重定向(Fallback)到 index.html,让前端路由接管后续的解析。

Q3:replaceState 的典型应用场景是什么?

参考回答: 常用于重定向纠正 URL。例如:

  1. 用户登录后,从登录页跳转到个人中心,可以使用 replaceState 替换掉登录页。这样用户在个人中心点击“后退”时,不会再回到登录页,而是回到登录之前的页面。
  2. 在搜索页修改筛选条件时,实时更新 URL 却不希望产生过多的历史记录导致用户后退困难。

JS-ES6 Class 类全方位进阶指南

前言

在 ES6 之前,JavaScript 开发者必须通过构造函数和原型链(prototype)来模拟“类”的行为,代码既冗长又容易出错。ES6 引入了 class 关键字,虽然它本质上是语法糖,但它让 JavaScript 的面向对象编程变得更加清晰和标准。

一、 基础:类的定义与实例化

class 将构造函数(constructor)和原型方法(speak)统一封装在一个大括号内。

class Animal {
    // 构造函数:每当 new 一个实例时自动调用
    constructor(name) {
        this.name = name; // 实例属性
    }
    // 原型方法:定义在 Animal.prototype 上,供所有实例共享
    speak() {
        console.log(`${this.name} speak!!!`);
    }
}

const dog = new Animal('dog');
dog.speak(); // dog speak!!!

二、 继承:extends 与 super 的力量

继承让子类可以复用父类的逻辑。在子类中,super 是连接父类的桥梁。

核心规则

  1. 必须调用 super:子类如果没有自己的 constructor,引擎会默认添加。如果有,则必须在访问 this 之前调用 super() ,否则会报错。
  2. 方法复用:可以使用 super.methodName() 调用父类的普通方法。
class Animal {
    constructor(name) {
        this.name = name;
    }
    eat() {
        return `${this.name} needs food`;
    }
}

class Cat extends Animal {
    constructor(name, color) {
        // 1. 调用父类构造函数(传递 name)
        super(name); 
        this.color = color;
    }
    eatFood() {
        // 2. 通过 super 调用父类的原型方法
        console.log(`${this.name} is ${this.color}, ${super.eat()}`);
    }
}

const mimi = new Cat('mimi', 'white');
mimi.eatFood(); // mimi is white, mimi needs food

三、 静态成员:static 关键字

static 定义的属性和方法属于类本身,而不是实例。

注意事项

  • 不可继承:实例对象无法访问静态方法。
  • this 指向:静态方法中的 this 指向的是 类本身,而不是实例。
class Animal {
    static feet = 4; // 静态属性

    static eat() {
        // 注意:这里的 this 指向 Animal 类本身
        console.log(`${this.name} is eating`); // 这里的 this.name 输出的是 "Animal"(类的名字)
    }
}

const cat = new Animal();
console.log(cat.feet);      // undefined (实例无法访问)
console.log(Animal.feet);   // 4 (类直接访问)
Animal.eat();               // "Animal is eating"

四、 面试模拟题

Q1:ES6 的 class 声明和普通 function 构造函数有什么区别?

参考回答:

  1. 严格模式class 内部默认就是严格模式。
  2. 变量提升class 不存在变量提升(存在暂时性死区),必须先定义后使用。
  3. 调用方式class 必须配合 new 调用,直接调用会报错;而普通构造函数可以作为普通函数执行。
  4. 原型方法class 定义的方法是不可枚举的。

Q2:子类构造函数中为什么必须先调用 super()

参考回答:

在 ES5 的继承中,是先创建子类的 this,然后再将父类的方法属性添加到 this 上。

但在 ES6 的继承中,是先创建父类的实例对象 this(通过 super()),然后再用子类的构造函数修改这个 this。如果不调用 super(),子类就拿不到 this 对象。

Q3:如何用 ES5 模拟 class 的静态方法?

参考回答:

在 ES5 中,直接将方法挂载到构造函数函数名上即可:

function Animal() {}
Animal.eat = function() { console.log('eating'); }; // 模拟静态方法

五、 总结

关键字 用途 核心点
constructor 初始化实例 new 的时候自动触发
extends 建立原型继承 子类原型指向父类原型
super 指向父类 构造函数中必须首行调用
static 定义类私有成员 只能由类名直接调用

JS-深度解构 JavaScript 浅拷贝与深拷贝(附手写源码)

前言

在处理对象或数组时,我们经常遇到“改了 A,结果 B 也变了”的情况。这背后涉及到了内存地址、引用传递以及拷贝的深度问题。理解浅拷贝与深拷贝,是处理复杂数据流和状态管理(如 Redux, Vuex)的基础。

一、 核心概念对比

1. 引用赋值

直接使用 =,这不属于拷贝,只是让两个变量指向内存中的同一个地址

let obj1 = { a: 1 };
let obj2 = obj1; // 引用赋值
obj2.a = 2;
console.log(obj1.a); // 2 (相互影响)

2. 浅拷贝 (Shallow Copy)

创建一个新对象,拷贝其第一层属性。如果属性是基本类型,拷贝的是值;如果属性是引用类型,拷贝的是内存地址

  • 实现方式Object.assign()、扩展运算符 ...Array.prototype.slice()

3. 深拷贝 (Deep Copy)

创建一个新对象,递归地拷贝所有层级的属性。两个对象在内存中完全独立,互不影响。


二、 深拷贝的两种主流实现

方法 1:JSON 序列化(简单但有陷阱)

思路JSON.parse(JSON.stringify(obj))

  • 优点:简单快捷,一行代码搞定。

  • 缺点(面试常考点)

    1. 会忽略 undefinedsymbol
    2. 会忽略 function(函数无法被序列化)。
    3. Date 对象会变成字符串。
    4. 无法处理循环引用的对象(会报错)。
const obj1 = { 
    body: { a: 10 },
    say: function(){ console.log('hello') } 
};
const obj2 = JSON.parse(JSON.stringify(obj1));

console.log(obj2.say); // undefined (函数丢失了!)

方法 2:递归手动实现(面试必考)

要写出一个完美的 deepClone,需要考虑:数组兼容、递归调用、原型属性过滤

/**
 * 深拷贝递归实现
 * @param {Object} target 目标对象
 * @returns {Object}
 */
function deepClone(target) {
    // 1. 如果不是对象或者是 null,直接返回
    if (typeof target !== 'object' || target === null) {
        return target;
    }

    // 2. 初始化返回结果(判断是数组还是对象)
    const result = Array.isArray(target) ? [] : {};

    // 3. 遍历目标对象
    for (let key in target) {
        // 4. 确保只遍历对象自身的属性,不包含原型链
        if (target.hasOwnProperty(key)) {
            // 5. 如果子属性还是对象,递归调用
            if (target[key] && typeof target[key] === 'object') {
                result[key] = deepClone(target[key]);
            } else {
                // 6. 基本类型则直接赋值
                result[key] = target[key];
            }
        }
    }
    return result;
}

// 测试
const original = { name: 'ouyang', arr: [1, 2], fn: () => {} };
const cloned = deepClone(original);
cloned.arr[0] = 999;

console.log(original.arr[0]); // 1 (互不影响,成功!)

三、 面试模拟题

Q1:Object.assign() 是深拷贝还是浅拷贝?

参考回答:浅拷贝。它只拷贝源对象自身的可枚举属性到目标对象。如果源对象的属性值是一个指向对象的引用,它也只拷贝那个引用地址。

Q2:如何解决深拷贝中的“循环引用”问题?

参考回答: 在递归实现中,可以使用一个 WeakMap 来存储已经拷贝过的对象。每次拷贝前先在 WeakMap 中查找,如果已经存在,则直接返回该对象,避免死循环。

Q3:为什么 JSON.stringify 无法拷贝函数?

参考回答: 因为 JSON 是一种数据交换格式,其标准中只定义了数字、字符串、布尔值、数组、对象和 null。函数属于代码逻辑而非数据,因此在 JSON 序列化规范中被排除在外。


四、 总结:我该选哪种方案?

  1. 处理简单的纯数据:用 JSON.parse(JSON.stringify(obj)),效率最高。
  2. 处理包含函数或特殊对象的复杂数据:使用手写的 deepClone 或者成熟的第三方库如 Lodash_.cloneDeep()
  3. 高性能需求:如果数据量极大且只有一层,优先使用扩展运算符 {...obj}

JS-彻底告别跨域烦恼:从同源策略到 CORS 深度实战

前言

在 Web 开发中,“跨域”是每个前端开发者绕不开的坎。当你看到控制台报出 Access-Control-Allow-Origin 错误时,其实是浏览器的同源策略在起作用。本文将带你深度解析跨域的本质,并掌握主流的解决方案。

一、 什么是跨域?

1. 同源策略 (Same-origin policy)

跨域问题的根源是浏览器为了安全而实施的同源策略。所谓“同源”,是指两个 URL 的以下三部分完全相同:

  • 协议:http 、 https
  • 域名 :域名就是我们每次访问网站输入的网址,每个域名都对应了一个IP地址,浏览器会通过域名解析来获取这个IP地址,例如www.test.com
  • 端口号 :80 、 8080

2. 域名解析小科普

域名是 IP 地址的“外壳”。

  • 顶级域名:.com, .cn
  • 一级域名:test.com
  • 二级域名www.test.com
  • 注意:一级域名和二级域名之间、二级域名和三级域名之间,统统属于跨域!比如在www.test.com网页使用 XMLHttpRequest 请求time.test.con的页面内容,由于它们不是同一个源,所以就涉及到了跨域(在 A 站点中去访问不同源的 B 站点的内容)。默认情况下,跨域请求是不被允许的,你可以看下面的示例代码:

二、 解决方案一:JSONP

1. 实现原理

利用 <script> 标签的 src 属性不受同源策略限制的特性。通过动态创建 script 标签,发送一个带有 callback 参数的 GET 请求。

2. 代码实现

前端逻辑:

btn.click(() => {
  var script = document.createElement("script");// 创建 scrip 标签
  script.src = `http://localhost:3000?callback=show`;// 添加 src 请求路径
  document.body.appendChild(script);
  script.onload = function(){
    document.body.removeChild(script)
  }
});

//这个函数就是回调函数,它会拼接到src属性中,并对数据进行操作
function show(result) {
  // ...
console.log("获取到的数据:", result);
}

服务端逻辑:

const http = require("http")
const url = require("url")
http.createServer(
  (req,res)=>{
    var callback = url.parse(req.url,true).query.callback;
    var severData = "xxxxxxxx";
    severData = JSON.stringify(severData)
    res.writeHead(200,{
      "Content-Type": "text/plain;charset=utf-8"
    });
    res.write(`${callback}(${severData})`);
    res.end();
  }
).listen(80)

局限性仅支持 GET 请求,不安全,且无法处理复杂的报错信息。


三、 解决方案二:CORS (现代的标准方案)

CORS(跨域资源共享)是目前的标准解法。它将请求分为简单请求非简单请求

1. 简单请求

条件:方法为 GET/POST

  • 流程:浏览器直接发起请求,并在 Header 中带上 Origin
  • 服务端:通过返回 Access-Control-Allow-Origin 来告知浏览器是否放行。

2. 非简单请求(预检请求)

条件:包含 PUT/DELETE 方法。

  • 流程:浏览器会先发送一个 OPTIONS 方法的“预检请求”。

  • 关键字段

    • Access-Control-Max-Age: 设置预检请求的缓存时间(秒),避免每次请求都多发一次 OPTIONS,优化性能。

3. Nginx 服务端配置示例

server {
    listen 80;
    location / {
        # 允许跨域的域名,建议生产环境指定具体域名而非 *
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
        
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Max-Age' 1728000;
            return 204;
        }
    }
}

四、 面试模拟题

Q1:为什么要有同源策略?如果没有会怎样?

参考回答:

同源策略主要是为了防止 CSRF(跨站请求伪造) 攻击。如果没有同源策略,黑客的网页可以随意读取你银行网页的 Cookie 或 DOM 内容,从而冒充你发送请求或窃取敏感信息。

Q2:CORS 预检请求(OPTIONS)在什么情况下会触发?

参考回答:

当请求满足以下任意条件时会触发预检:

  1. 使用了 PUTDELETECONNECTOPTIONSTRACEPATCH 方法。
  2. 设置了非简单的 Header 字段(如 Authorization、自定义 Token)。
  3. Content-Type 的值不属于 application/x-www-form-urlencodedmultipart/form-datatext/plain

Q3:如何解决跨域时 Cookie 无法携带的问题?

参考回答:

  1. 前端XMLHttpRequestfetch 需设置 withCredentials: true
  2. 服务端:设置响应头 Access-Control-Allow-Credentials: true
  3. 注意:当开启凭证携带时,Access-Control-Allow-Origin 不能设置为 * ,必须指定具体的域名。

五、 总结

方案 原理 优点 缺点
JSONP <script> 标签不受限 兼容性极好(老浏览器) 只支持 GET,安全性差
CORS 服务端 Header 授权 正式标准,支持所有方法 需服务端配合,有预检开销

JS-深度解构JS事件循环(Event Loop)

前言

为什么 JavaScript 是单线程的却能处理异步 IO?为什么 setTimeout 并不总是准时?本文将从宏观的执行栈、任务队列,一直深入到浏览器底层的任务调度逻辑,带你彻底看透事件循环。

一、 为什么需要事件循环?

JavaScript 的核心是单线程的,这意味着它只有一个主线程来处理 DOM 解析、样式计算、脚本执行等。如果某个任务耗时过长,页面就会“卡死”。为了协调同步任务与异步任务(输入事件、网络请求、定时器),浏览器引入了事件循环系统来统一调度和处理这些任务。


二、 核心组件:执行栈与任务队列

1. 执行栈 (Execution Stack)

当多个方法被调用的时候,因为js是单线程的,所以每次只能执行一个方法,于是这些方法被排到了一个单独的地方,这个地方就是执行栈。执行栈里面执行的都是同步的操作。

2. 事件队列 (Task Queue)

  • 在js执行过程中如果遇到异步事件(如 Ajax、定时器),就会首先将这个异步事件交给对应的浏览器模块(如网络进程),继续执行执行栈里面的任务。
  • 当异步事件返回结果后,js不会立即执行这个回调,会将事件加入到事件队列中,只有当执行栈里面的全部执行完以后,主线程才会去查找事件队列中是否有任务。
  • 如果有,那么主线程会取出事件队列里面排在最前面的事件,将这个事件对应的回调加入到执行栈中,然后执行其中的同步代码。然后在继续观察执行栈里面是否有任务,依次反复...就形成了一个无限的循环。
  • 这就是这个过程被称为事件循环(Event loop)的原因。

循环逻辑:

  1. 检查执行栈是否为空。
  2. 若为空,从事件队列头部取出一个任务推入执行栈。
  3. 循环往复。

三、 异步任务的“等级”:宏任务与微任务

并非所有的异步任务优先级都一样。在同一次循环中,微任务永远在下一次宏任务之前执行!!!

类型 包含任务 执行时机
宏任务 (MacroTask) setTimeout, setInterval, ajax, dom事件 每次事件循环开始时处理一个
微任务 (MicroTask) Promise.then/catch, MutaionObserver, process.nextTick (Node.js) 当前执行栈清空后,立即清空整个微任务队列

注意: new Promise() 构造函数内部的代码是同步执行的,只有 .then().catch() 里的回调才是微任务。(后续会专门出一篇promise相关文章)


四、 底层揭秘:定时器是如何实现的?

很多开发者认为 setTimeout 是直接进入消息队列的,但浏览器底层其实维护了一个延迟执行队列 (Delayed Incoming Queue)

1. 任务数据结构

当调用 setTimeout 时,渲染进程内部会创建一个任务结构体:

struct DelayTask{
  int64 id;
  CallBackFunction cbf;
  int start_time;
  int delay_time;
};

2. 执行循环模拟

浏览器的主线程循环逻辑伪代码如下:

void MainThread() {
  for(;;) {
    // 1. 执行普通消息队列中的一个任务 (宏任务)
    Task task = task_queue.takeTask();
    ProcessTask(task);
    
    // 2. 执行微任务队列 (本阶段由 JS 引擎控制)
    // ProcessMicrotasks(); 

    // 3. 执行延迟队列中到期的任务 (定时器任务在此处理)
    ProcessDelayTask();

    if(!keep_running) break; 
  }
}

关键点: 浏览器会在处理完一个普通宏任务后,去检查延迟队列中是否有任务到期(ProcessDelayTask),并依次执行它们。


五、 面试模拟题

Q1:为什么 setTimeout(fn, 0) 并不一定是 0ms 后执行?

参考回答:

  1. 浏览器最小限制:HTML5 规范规定,如果定时器嵌套超过 5 层,最小延迟为 4ms。
  2. Event Loop 阻塞:由于定时器任务是在 ProcessDelayTask 中处理的,如果当前的宏任务(比如一个复杂的计算循环)执行时间过长,主线程就无法及时跳转到延迟队列的检查步骤,导致定时器推迟执行。

Q2:说出以下代码的打印顺序:

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

参考回答:

1 -> 4 -> 3 -> 2。

  • 1, 4 是同步任务,直接输出。
  • 3 是微任务,在当前脚本(宏任务)执行完后立即执行。
  • 2 是下一次宏任务。

Q3:MutationObserver 属于什么任务?它有什么应用场景?

参考回答:

MutationObserver 属于微任务。它用于监听 DOM 树的变化。由于它是微任务,它会在 DOM 变化引起的多次修改全部完成后,在浏览器重新渲染之前异步执行,这比传统的 Mutation Events 性能更高,且不会阻塞主线程渲染。


六、 总结建议

  • 理解微任务的优先级:微任务是在当前宏任务结束后的“插队”行为,适合处理需要立即反馈的异步逻辑。

JS-new 操作符

前言

在 JavaScript 面向对象编程中,new 关键字是实例化对象的核心。面试官常常通过“手写 new”来考察你对原型链this 绑定以及构造函数返回值的理解。本文将带你从原理到实现,彻底搞懂 new 背后的魔法。

一、 new 到底干了什么?

当我们使用 new Person() 时,JS 引擎在背后默默执行了以下 4 个步骤

  1. 创建一个新对象:在内存中创建一个新的空对象(例如 obj = {})。

  2. 链接原型:将新对象的 __proto__ 属性指向构造函数的 prototype,从而实现原型继承(让实例能访问原型上的方法)。

  3. 绑定 this:将构造函数内部的 this 绑定到这个新对象上,并执行构造函数(为新对象添加属性)。

  4. 返回对象

    • 如果构造函数显式返回了一个对象(或函数),则返回该结果。
    • 如果构造函数没有返回对象(返回基本类型或无返回值),则返回步骤 1 创建的新对象

二、 手写 myNew 实现

根据上述原理,我们可以实现一个自己的 myNew 函数。

/**
 * 手写 new 操作符
 * @param {Function} Constructor 构造函数
 * @param  {...any} args 传递的参数
 */
function myNew(Constructor, ...args) {
  // 1. 创建一个新对象,并将其原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 将构造函数的 this 绑定到新对象上,并执行构造函数
  const result = Constructor.apply(obj, args);

  // 3. 处理返回值逻辑 (这是面试中最容易忽视的细节!)
  // 如果构造函数返回的是对象(不为null)或函数,则返回该结果;否则返回新创建的 obj
  if ((typeof result === 'object' && result !== null) || typeof result === 'function') {
    return result;
  }
  
  // 4. 返回新对象
  return obj;
}

测试用例:

function Person(name, age) {
  this.name = name;
  this.age = age;
  // 情况 1: 没有返回值(默认返回 this)
}

function Student(name) {
  this.name = name;
  // 情况 2: 返回一个对象
  return { name: 'Special Student', grade: 100 };
}

function NumberObj() {
  this.a = 1;
  // 情况 3: 返回一个基本类型
  return 123;
}

// 测试 1:正常情况
const per = myNew(Person, 'Ouyang', 23);
console.log(per); // Person { name: 'Ouyang', age: 23 }
console.log(per instanceof Person); // true

// 测试 2:构造函数返回对象
const stu = myNew(Student, 'XiaoMing');
console.log(stu); // { name: 'Special Student', grade: 100 } (this 被忽略了)

// 测试 3:构造函数返回基本类型
const num = myNew(NumberObj);
console.log(num); // NumberObj { a: 1 } (返回值 123 被忽略)

三、 深度解析:返回值陷阱

这是面试中最常挖的坑。

  • 场景 A:构造函数内部没有 return,或者 return 一个基本数据类型(Number, String, Boolean, null, undefined)。

    • 结果new 操作符会忽略这个返回值,直接返回新创建的实例对象
  • 场景 B:构造函数内部 return 一个引用类型(Object, Array, Function)。

    • 结果new 操作符会直接返回这个引用类型,新创建的实例对象会被丢弃(且 this 上的属性赋值也会失效)。

四、 面试模拟题(挑战一下)

Q1:Object.create()new 有什么区别?

参考回答:

  • new:不仅创建新对象并继承原型,还会执行构造函数,进行属性初始化。
  • Object.create():只负责创建一个新对象并继承原型,不会执行构造函数

Q2:为什么代码中建议使用 Object.create 而不是 obj.__proto__

参考回答: __proto__ 是非标准属性(虽然浏览器支持),直接修改它会破坏 JS 引擎的优化,严重影响性能。Object.create() 是 ES5 标准方法,更规范且性能更好。

Q3:如果构造函数返回 nullnew 出来的结果是什么?

参考回答: 结果是新创建的实例对象。 因为 typeof null === 'object',但 null 是个特殊值。在 new 的规范中,如果返回的是对象类型但值为 null,仍然会忽略它,返回实例对象。这就是为什么在手写代码中我们要判断 result !== null


结语

手写 new 是前端基础能力的试金石。理解了这 4 个步骤,你不仅能轻松应对面试,还能更深刻地理解 JavaScript 的继承机制。

如果你觉得这篇笔记对你有帮助,欢迎点赞收藏! 🚀

JS -彻底搞懂 call、apply、bind 的区别与应用

前言

在 JavaScript 中,this 的指向是动态的,这虽然灵活,但也常让我们头疼。而 callapplybind 就是我们手中的“魔法棒”,专门用来手动控制 this 的指向。它们有什么区别?分别在什么场景下使用?本文带你一探究竟。

一、 三大方法详解

这三个方法都挂载在 Function.prototype 上,这意味着所有的函数都可以调用它们。

1. call()

  • 作用:修改函数的 this 指向,并立即执行该函数。

  • 参数

    1. thisArgthis 需要绑定的对象。
    2. arg1, arg2, ...参数列表,直接按顺序传入。
  • 默认行为:如果不传 thisArg 或传 null/undefined,在非严格模式下指向 window

fn.call(obj, agr1,agr2,arg3,arg4,.....)

2. apply()

  • 作用:修改函数的 this 指向,并立即执行该函数。

  • 参数

    1. thisArgthis 需要绑定的对象。
    2. argsArray数组(或类数组) ,数组内的元素会被展开传入函数。
fn.apply(obj, [agr1,agr2,arg3,arg4,.....])

3. bind()

  • 作用:修改函数的 this 指向,但不会立即执行
  • 返回值:返回一个新的函数(称为绑定函数)。
  • 硬绑定bind 返回的新函数,其 this 指向一旦被绑定,后续再使用 callapply 都无法再次修改。
  • 参数:与 call 相同,接受参数列表。支持柯里化(预设部分参数)。
bind(thisArg, arg1, arg2, arg3, ...)

二、 核心区别对比(一张表看懂)

方法 执行时机 参数格式 返回值 核心场景
call 立即执行 参数列表 (arg1, arg2) 函数执行结果 对象继承、借用方法
apply 立即执行 数组 ([arg1, arg2]) 函数执行结果 数学计算、数组合并
bind 稍后执行 参数列表 (arg1, arg2) 新函数 事件绑定、回调函数

三、 代码实战与纠错

让我们通过一个经典的例子来看它们的具体表现。

const obj = {
  name: 'Original',
  fn: function(a, b) {
    console.log(this.name, a, b);
  }
}

const db = { name: 'DataBase' };

// 1. 原始调用
obj.fn(1, 2); 
// 输出: "Original" 1 2

// 2. call 调用:传参列表
obj.fn.call(db, 3, 4); 
// 输出: "DataBase" 3 4

// 3. apply 调用:传参数组
obj.fn.apply(db, [5, 6]); 
// 输出: "DataBase" 5 6

// 4. bind 调用:返回新函数,手动执行
const boundFn = obj.fn.bind(db, 7, 8);
boundFn(); 
// 输出: "DataBase" 7 8

// 5. bind 的连续修改无效性(面试坑点)
const doubleBind = obj.fn.bind(db).bind({ name: 'Error' });
doubleBind();
// 输出: "DataBase" undefined undefined (第二次 bind 无效)

四、 常见应用场景(面试加分项)

仅仅知道语法是不够的,面试官更看重你知道怎么用。

1. 数组求最大值 (apply)

利用 apply 接受数组参数的特性,结合 Math.max

const nums = [5, 10, 20, 1];
const max = Math.max.apply(null, nums); // 20
// ES6 写法: Math.max(...nums)

2. 类数组转数组 (call)

利用 call 借用数组的 slice 方法。

function func() {
  const args = Array.prototype.slice.call(arguments);
  console.log(args); // 变成了真数组
}

3. React/Vue 中的事件绑定 (bind)

防止回调函数在执行时 this 丢失(指向 undefinedwindow)。

this.handleClick = this.handleClick.bind(this);

五、 面试模拟题

Q1:callapply 的唯一区别是什么?

参考回答:

它们的唯一区别在于传参方式。call 需要把参数按顺序一个个传进去(参数列表),而 apply 需要把参数放在一个数组(或类数组)里传进去。助记口诀:"a" for array (apply), "c" for comma (call)。

Q2:为什么 bind 返回的函数,再次使用 call 无法修改 this

参考回答:

这涉及 bind 的内部实现。bind 返回的函数内部已经通过闭包锁定了 this(通常称为硬绑定)。也就是类似 return function() { return originalFn.apply(that, arguments) } 的结构。无论外部怎么 call,内部的 apply 永远使用的是第一次绑定的 that。

Q3:手写一个简单的 bind

      Function.prototype.myBind = function (context, ...args) {
        // 1. 保存当前的函数(this 指向原函数)
        const fn = this;
        // 2. 返回一个新的函数
        return function (...innerArgs) {
          // 3. 将预设参数和新参数合并,并用 apply 执行原函数
          return fn.apply(context, args.concat(innerArgs));
        };
      };
      const obj = {
        name: "Original",
        fn: function (a, b) {
          console.log(this.name, a, b);
        },
      };
      const boundFn = obj.fn.myBind({ name: 'DataBase' }, 7, 8);
      boundFn();
❌