跳到主要内容

JavaScript

什么是闭包?有什么作用?​

  • 能够访问其它函数内部变量的函数,称为闭包
  • 能够访问自由变量的函数,称为闭包

至于闭包的使用场景,其实在日常开发中使用到是非常频繁的

  • 防抖节流函数
  • 定时器回调
  • 等就不一一列举了

优点

闭包帮我们解决了什么问题呢 内部变量是私有的,可以做到隔离作用域,保持数据的不被污染性

缺点

同时闭包也带来了不小的坏处 说到了它的优点内部变量是私有的,可以做到隔离作用域,那也就是说垃圾回收机制是无法清理闭包中内部变量的,那最后结果就是内存泄漏

什么是高阶函数?什么是高阶组件?​

高阶函数(Higher-Order Function):​

在 JavaScript 中,高阶函数是指满足以下条件之一或两者的函数:

  1. 接受一个或多个函数作为参数。
  2. 返回一个新函数。

高阶函数可以像操作其他数据类型一样操作函数,这使得函数可以作为参数传递,也可以作为返回值返回。这种函数式编程的思想使得代码更加灵活、模块化和可复用。

示例:

// 高阶函数示例:接受函数作为参数
function greet(name) {
return `Hello, ${name}!`;
}

function greetUser(greetFunction, userName) {
return greetFunction(userName);
}

console.log(greetUser(greet, "Alice")); // Output: Hello, Alice!

// 高阶函数示例:返回一个函数
function multiplier(factor) {
return function(num) {
return num * factor;
};
}

const double = multiplier(2);
console.log(double(5)); // Output: 10
高阶组件(Higher-Order Component):​

在 React 中,高阶组件是一个函数,接受一个组件作为参数,并返回一个新的增强型组件。高阶组件的作用是复用组件逻辑,对现有组件进行包装以添加额外的功能或特性。

通过高阶组件,可以将重复的逻辑提取出来,并应用到多个组件中,实现代码的重用和逻辑的解耦。

示例:

// 高阶组件示例
function withLogger(Component) {
return function(props) {
console.log(`Logging props: ${JSON.stringify(props)}`);
return <Component {...props} />;
};
}

const EnhancedComponent = withLogger(MyComponent);

// 使用高阶组件包装后的组件
<EnhancedComponent />

高阶组件在 React 中被广泛应用,例如用于添加认证、日志记录、性能监控等功能,同时保持组件的纯粹性和可重用性。通过高阶组件,可以更好地实现代码的组织和复用。

什么是原型?什么是原型链?​

原型(Prototype):​

在 JavaScript 中,每个对象都有一个指向另一个对象的引用,这个对象就是该对象的原型。原型是 JavaScript 实现继承的基础,它允许对象继承另一个对象的属性和方法。

每个对象都有一个内部属性 [[Prototype]] 指向其原型对象,可以通过 Object.getPrototypeOf(obj) 获取对象的原型。当访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找直到找到匹配的属性或方法。

原型链(Prototype Chain):​

原型链是一种用于实现对象之间继承和共享属性的机制。在 JavaScript 中,每个对象都有一个原型,并且这个原型也可以有自己的原型,形成一个原型链。

当试图访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达原型链的顶端(即 Object.prototype)。这样,对象可以共享原型链上的属性和方法。

示例:

// 创建一个对象 obj,它的原型是 Object.prototype
const obj = {};

// obj 没有自己的属性 x,但可以访问到 Object.prototype 上的属性 toString
console.log(obj.toString()); // 输出:"[object Object]"

// 示例中的原型链:obj --> Object.prototype --> null

原型链的概念使得 JavaScript 中的对象能够实现继承和共享,它是 JavaScript 中实现对象之间关系的重要机制之一。

什么是防抖和节流?有什么区别?​

防抖:多次触发防抖事件,从最后一次开始计时,持续设置的时间后执行。触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间

节流:多次触发节流事件,会按照固定的事件间隔计算,时间之内的会忽略,超过固定时间才会再次执行。高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率

一个形象的比喻:打王者,防抖等于回程,节流等于放大招。

//防抖
function debounce(fn,time){
var timer;

return function(){
var _this = this;
var args = arguments;
if(timer){
clearTimeout(timer);
}
timer = setTimeout(()=> {
fn.apply(_this,args)
}, time)
}
}

//节流
function throttle(fn,time){
var start = Date.now();//这个等于0,表示点击后会立即触发,如果是Date.now()则代表第一次点击后等待time才出发

return function(){
var _this = this;
var args = arguments;
var now = Date.now();
if(now - start > time){
fn.apply(_this,args);
start = now;
}
}
}

介绍一下模块化?AMD/CMD/UMD/Commonjs/ES6 的区别?​

模块化是对代码的一种管理和使用方式。

AMD:Async Module Define,异步加载模块,模块加载不影响后面代码的运行。通过define(id?, dependencies?, factory);来定义模块,通过require([module], callback);加载模块。特点:依赖必须提前声明好。

define('./index.js',function(code){
// code 就是index.js 返回的内容
})

优点:

  1. 适合在浏览器环境中异步加载模块
  2. 可以并行加载多个模块

缺点:

  1. 提高了开发成本
  2. 不符合通用的模块化思维方式

CMD:Common Module Define,同意模块定义,一个模块就是一个文件。通过define(factory);定义模块,通过seajs.use([module], callback);加载模块。使用 seaJS 来编写模块化,特点:支持动态引入依赖文件。

define(function(require, exports, module) {
var indexCode = require('./index.js');
});

优点:可以很容易在 node 中运行

缺点:依赖 SPM 打包,模块的加载逻辑偏重

UMD:Universal Module Definition,UMD 是 AMD 和 CommonJS 的一个糅合。AMD 是浏览器优先,异步加载;CommonJS 是服务器优先,同步加载。先判断是否支持 node 的模块,支持就使用 node;再判断是否支持 AMD,支持则使用 AMD 的方式加载。这就是所谓的 UMD。

CommonJS:主要用来 node 上,每个文件就是一个模块,每个文件有自己的作用域。通过 module.exports 暴露成员,通过 require 引入其他模块。

优点:

  1. 简单并且容易使用
  2. 服务器端模块便于重用

缺点:

  1. 同步的模块加载方式不适合在浏览器环境中
  2. 不能非阻塞的并行加载多个模块

ES6 Module:ES6 模块的设计思想是尽量的 静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。在 ES6 中,我们使用 export 关键字来导出模块,使用 import 关键字来引入模块。

//导入
import { stat, exists, readFile } from "fs";
//导出
let firstName = "Zhou";
let lastName = "ShuYi";
let year = 1994;
export { firstName, lastName, year };

优点:容易进行静态分析

缺点:原生浏览器端还没有实现该标准

AMD 和 CMD 的区别​
  1. 对于依赖的模块,AMD 是 提前执行,CMD 是 延迟执行。
  2. AMD 推崇 依赖前置,CMD 推崇 依赖就近。
  3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
ES6 模块与 CommonJS 模块的差异​
  1. CommonJS 模块输出的是一个 值的拷贝,ES6 模块输出的是 值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJS 模块的 require() 是 同步加载 模块,ES6 模块的 import 命令是 异步加载,有一个独立的模块依赖的解析阶段。

什么是作用域?JS 有哪些作用域?​

执行上下文(EC)

作用域是变量和函数代码生效的范围。

JS 包括:全局作用域、函数作用域、块级作用域

什么是作用域链?什么是 GO/AO/VO/[[SCOPE]]/EC?​

代码执行过程中,访问一个变量是,会在当前作用域查找,如果找不到,会在当前执行上下文的作用域链一直往上找,直到找到最后为止。

这些都是执行上下文中的概念:

GO:Global Object 全局执行上下文中的变量对象

AO:Active Object 激活的变量对象,一般在正在执行的上下文中会用 AO 来保存

VO: Variable Object 变量对象,每个函数作用域中都会保存 VO 来保存参数和局部变量等

[[SCOPE]]:每个函数定义时都会有一个内部属性[[scope]],用来保存作用域链的信息。

EC: Execution Context 执行上下文,包括变量对象、作用域链、this 指向等信息

EC 包含三部分:

  • VO
  • [[scope]]
  • this

EC 可以分为全局执行上下文、函数执行上下文、eval 执行上下文

箭头函数和普通函数的区别?​

1.箭头函数不能 new,也就是不能当做构造函数,普通函数可以

2.箭头函数使用()=> {}来定义函数,不同函数用 function

3.箭头函数没有自己的 this,他的 this 执行当前上下文。而普通函数的 this 指向调用函数的上下文

4.箭头函数没有 arguments

new 一个函数后,都发生了什么事情?​

  1. 创建一个空对象,这个对象就是最后要返回的实例对象
  2. 获取构造函数,自定义 new 一般取第一个参数,但是 new 实际上获取的是 new 后面的构造函数
  3. 将实例的__proto__指向构造函数的 prototype
  4. 使用 apply 调用构造函数,传入实例对象以及参数,根据返回的结果判断返回实力对象还是结果
function myNew(constructor, ...args) {
// 1. 创建一个新对象
const obj = Object.create(constructor.prototype);
//或者
// const obj = Object.create(null);
// obj.__proto__ = constructor.prototype;

// 2. 执行构造函数,并将 `this` 指向新对象
const result = constructor.apply(obj, args);

// 3. 如果构造函数返回一个对象,返回这个对象;否则返回新创建的对象
return (result !== null && typeof result === 'object') ? result : obj;
}

call 和 apply 有什么区别?​

都是用来改变函数体内的 this 指向,并且可以传入参数。

不同点:

call 需要传入很多参数,apply 传入一个参数数组。

记忆点:call 类似于打电话,只能一个一个打电话,apply 类似于发短信,可以一次用数组发给多个人。

js 中++在变量前面和后面的区别?​

在 JavaScript 中,++ 操作符用于对变量进行自增操作,但它在变量前面和后面的使用方式有所不同,这会影响表达式的值。

前置自增(++variable)​

  • 语法: ++x
  • 效果: 先将 x 的值增加 1,然后返回增加后的值。

后置自增(variable++)​

  • 语法: x++
  • 效果: 先返回 x 的当前值,然后再将 x 的值增加 1。

示例代码​

let a = 5;
let b = ++a; // 前置自增
console.log(a); // 输出: 6
console.log(b); // 输出: 6

let c = 5;
let d = c++; // 后置自增
console.log(c); // 输出: 6
console.log(d); // 输出: 5

总结​

  • 前置自增: 先增加,再使用新值。
  • 后置自增: 先使用当前值,再增加。

介绍一下浏览器中的事件循环机制?和 Node 中的一样吗?有什么区别?​

image

img

Event Loop 研究

如何判断变量的类型?有哪些方法?​

1、基本类型 typeof 复杂类型 instanceof

2、Object.prototype.toString.call();

3、Array.isArray()判断数组

typeof 和 instanceOf 有什么区别?​

typeof 用于检测变量的数据类型。主要用于基本数据类型(如 number、string、boolean、undefined)、function 和 object(包括数组和 null)的判断。

instanceof 用于判断一个对象是否是某个构造函数的实例。主要用于检测引用类型,比如对象、数组等,以及自定义的构造函数。

Jquery 如何实现链式调用?​

jQuery 是一个流行的 JavaScript 库,它通过实现链式调用(chaining)来简化 DOM 操作和事件处理。链式调用使得可以在单个语句中依次调用多个方法,而不需要每次都重新选择元素。

实现链式调用的关键点:​

  1. 返回 this: 在每个方法的最后,要返回当前 jQuery 对象 this,以便能够继续在该对象上调用其他方法。
  2. 方法内部操作: 在每个方法中对目标元素进行操作,并返回 this 以保持链式调用。

示例:​

// 假设有一个按钮元素
const $button = $('#myButton');

// 链式调用示例
$button.css('color', 'red').addClass('highlight').on('click', function() {
alert('Button clicked!');
});

// 上述代码等价于以下非链式调用方式
$button.css('color', 'red');
$button.addClass('highlight');
$button.on('click', function() {
alert('Button clicked!');
});

在上面的示例中,$button.css('color', 'red').addClass('highlight').on('click', function() { ... }) 实现了链式调用,依次对按钮元素进行样式修改、添加类名和绑定点击事件。每个方法在内部对目标元素进行操作,并返回 this,以便后续方法能够继续在该对象上调用。

在 jQuery 内部,每个方法都会返回 this,这样就可以在一个 jQuery 对象上连续调用多个方法,形成链式调用。链式调用使得代码更加简洁和易读,减少了重复选择元素的操作,提高了代码的可读性和可维护性。

通过链式调用,jQuery 简化了 DOM 操作和事件处理,是 jQuery 这个库受欢迎的一个重要原因之一。

为什么 JS 是单线程?​

JavaScript 作为一种单线程语言,这意味着它一次只能执行一个任务。这种设计选择是基于 JavaScript 最初设计的用途和运行环境的考虑,主要有以下几个原因:

  1. 简单性: 单线程使得 JavaScript 的设计和执行更加简单,避免了复杂的多线程同步和竞争条件问题。

  2. 浏览器模型: JavaScript 最初是作为浏览器脚本语言而设计的,浏览器是单线程的,主要负责用户界面的渲染和响应用户交互。因此,JavaScript 也采用了单线程执行模型。

  3. 事件驱动: JavaScript 基于事件驱动的模型,通过事件循环实现异步编程。单线程的设计使得事件循环能够简单而高效地处理事件和回调函数。

  4. DOM 操作: 大多数 DOM 操作都会改变页面的外观和结构,因此需要在单个线程中依次执行,以避免导致页面状态不一致或渲染问题。

  5. 避免竞态条件: 多线程会引入复杂的同步问题,如竞态条件、死锁等。单线程可以避免这些问题,简化了编程模型。

虽然 JavaScript 主线程是单线程的,但是通过 Web Workers 可以创建多线程的 JavaScript 环境,以便执行一些耗时操作,但这些 Web Workers 是在后台运行的,不能直接操作 DOM,通信也需要通过消息传递来实现。

总的来说,JavaScript 作为一种单线程语言,这种设计使得它更容易学习和使用,并且能够有效地处理大多数 Web 应用程序的需求。虽然单线程有一些限制,但通过事件驱动和异步编程模型,JavaScript 可以实现非阻塞的并发操作,保持用户界面的响应性。

什么是 MutationObserver?有什么作用?​

MutationObserver 是 JavaScript 中的一个内置对象,用于监视 DOM 树的变化。通过 MutationObserver,开发者可以异步监听 DOM 树的变化,并在发生变化时执行特定的回调函数。这个功能在实时监测 DOM 变化、响应用户操作、实现动态 UI 等场景中非常有用。

MutationObserver 的作用:​
  1. 监视 DOM 变化: 可以监视 DOM 结构的变化,如节点的添加、删除、属性的修改等。
  2. 实时响应: 可以实时响应 DOM 变化,而不需要通过轮询来检查 DOM 的状态变化。
  3. 性能优化: 监听 DOM 变化的回调是异步执行的,可以减少因为频繁操作 DOM 而导致的性能问题。
  4. 动态 UI: 可以根据 DOM 的变化来动态更新页面的内容或样式。
使用 MutationObserver 的步骤:​
  1. 创建 MutationObserver 实例: 使用 new MutationObserver(callback) 创建一个 MutationObserver 实例,传入一个回调函数作为参数。
  2. 配置观察目标: 使用 observe(target, options) 方法配置要观察的目标节点和观察选项。
  3. 处理变化: 当目标节点或其子节点发生变化时,触发回调函数进行相应的处理。

示例:​

// 选择要观察的目标节点
const targetNode = document.getElementById('target');

// 创建一个 MutationObserver 实例,传入回调函数
const observer = new MutationObserver(function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
} else if (mutation.type === 'attributes') {
console.log('Attributes of the target node have been changed.');
}
}
});

// 配置观察目标和观察选项
const config = { attributes: true, childList: true, subtree: true };
observer.observe(targetNode, config);

在上述示例中,MutationObserver 监视了目标节点的子节点变化和属性变化。当目标节点或其子节点发生变化时,会触发回调函数,并根据变化的类型执行相应的操作。

通过 MutationObserver,开发者可以更加灵活地监视 DOM 的变化,从而实现一些高级的 DOM 操作和交互。

什么是 MessageChannel?有什么用?​

MessageChannel 是 HTML5 中引入的一个 API,用于在两个不同的执行上下文(比如两个窗口、两个 iframe 或者一个 window 和一个 worker)之间建立通信通道。通过 MessageChannel,可以在这些执行上下文之间安全地传递消息。

使用 MessageChannel 的步骤:​
  1. 创建 MessageChannel 实例: 在一个执行上下文中创建 MessageChannel 实例。
  2. 获取端口: 分别从 MessageChannel 实例中获取两个 MessagePort,一个用于发送消息,另一个用于接收消息。
  3. 发送消息: 通过发送端口的 postMessage 方法向另一个执行上下文发送消息。
  4. 接收消息: 在接收端口上监听 message 事件,处理接收到的消息。
// 创建 MessageChannel 实例
const channel = new MessageChannel();

// 获取发送和接收端口
const port1 = channel.port1;
const port2 = channel.port2;

// 在 window1 中发送消息到 window2
port1.postMessage('Message from window1');

// 在 window2 中接收消息
port2.onmessage = function(event) {
console.log('Message received in window2:', event.data);
};

requestAnimationFrame 有什么作用?​

window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。该方法属于宏任务,所以会在执行完微任务之后再去执行。

若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFrame()。requestAnimationFrame() 是一次性的。

requestAnimationFrame(callback) //

这个回调函数只会传递一个参数:一个 DOMHighResTimeStamp 参数,用于表示上一帧渲染的结束时间(基于 time origin 的毫秒数) 时间戳是一个以毫秒为单位的十进制数字,最小精度为 1 毫秒。

返回值,请求 ID 是一个 long 类型整数值,是在回调列表里的唯一标识符。这是一个非零值,但你不能对该值做任何其他假设。你可以将此值传递给 window.cancelAnimationFrame() 函数以取消该刷新回调请求。

作用:主要用来为动画提高性能和流畅度,确保动画在每次重绘之前都可以执行,获得较好的效果,相对于 settimeout 和 setintervel 更加流程,性能也更好。在浏览器处于非活动状态或者被隐藏时,会暂停执行,节省资源和性能。

requestIdleCallback 有什么作用?​

window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。requestIdleCallback 是一个宏任务(macro task).

你可以在空闲回调函数中调用 requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。

requestIdleCallback(callback)
requestIdleCallback(callback, options)

callback

一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。

options 可选

包括可选的配置参数。具有如下属性:

  • timeout:如果指定了 timeout,并且有一个正值,而回调在 timeout 毫秒过后还没有被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。

返回值:一个 ID,可以把它传入 Window.cancelIdleCallback() 方法来结束回调。

requestIdleCallback(function (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
task();
}
});

有没有用 npm 发布过 package,如何发布​

1、注册 npm 账号

2、本地登录 npm 账号npm login

3、在package.json中指定发布的文件和文件夹

{
"name": "pkg-xxx",
"version": "0.0.1",
"main": "lib/index.js",
"module": "esm/index.js",
"typings": "types/index.d.ts",
"files": [
"CHANGELOG.md",
"lib",
"esm",
"dist",
"types",
],
...
}

4、运行npm publish --registry=https://registry.npmjs.org

js 代码压缩 minify 的原理是什么​

通过 AST 分析,根据选项配置一些策略,来生成一颗更小体积的 AST 并生成代码。

目前前端工程化中使用 terser 和 swc 进行 JS 代码压缩,他们拥有相同的 API。

常见用以压缩 AST 的几种方案如下:

去除多余字符: 空格,换行及注释​
// 对两个数求和
function sum (a, b) {
return a + b;
}

此时文件大小是 62 Byte, 一般来说中文会占用更大的空间。

多余的空白字符会占用大量的体积,如空格,换行符,另外注释也会占用文件体积。当我们把所有的空白符合注释都去掉之后,代码体积会得到减少。

去掉多余字符之后,文件大小已经变为 30 Byte。 压缩后代码如下:

function sum(a,b){return a+b}

替换掉多余字符后会有什么问题产生呢?

有,比如多行代码压缩到一行时要注意行尾分号。

压缩变量名:变量名,函数名及属性名​
function sum (first, second) {
return first + second;
}

如以上 first 与 second 在函数的作用域中,在作用域外不会引用它,此时可以让它们的变量名称更短。但是如果这是一个 module 中,sum 这个函数也不会被导出呢?那可以把这个函数名也缩短。

// 压缩: 缩短变量名
function sum (x, y) {
return x + y;
}

// 再压缩: 去除空余字符
function s(x,y){return x+y}

在这个示例中,当完成代码压缩 (compress) 时,代码的混淆 (mangle) 也捎带完成。 但此时缩短变量的命名也需要 AST 支持,不至于在作用域中造成命名冲突。

解析程序逻辑:合并声明以及布尔值简化​

通过分析代码逻辑,可对代码改写为更精简的形式。

合并声明的示例如下:

// 压缩前
const a = 3;
const b = 4;

// 压缩后
const a = 3, b = 4;

布尔值简化的示例如下:

// 压缩前
!b && !c && !d && !e

// 压缩后
!(b||c||d||e)
解析程序逻辑: 编译预计算​

在编译期进行计算,减少运行时的计算量,如下示例:

// 压缩前
const ONE_YEAR = 365 * 24 * 60 * 60

// 压缩后
const ONE_YAAR = 31536000

以及一个更复杂的例子,简直是杀手锏级别的优化。

// 压缩前
function hello () {
console.log('hello, world')
}

hello()

// 压缩后
console.log('hello, world')

关于 JSON,以下代码输出什么​

const obj = {
a: 3,
b: 4,
c: null,
d: undefined,
get e() {},
};

console.log(JSON.stringify(obj));
// {"a":3,"b":4,"c":null}

对其中的 undefined,将在 JSON.stringify 时会忽略掉

而对于 get e 函数,由于返回的也是 undefined,所以也会被忽略

如果改成下面:

const obj = {
a: 3,
b: 4,
c: null,
d: undefined,
get e() {return 123},
};

console.log(JSON.stringify(obj));
// {"a":3,"b":4,"c":null,"e":123}

在 js 中如何把类数组转化为数组​

首先,什么是类数组(Array Like)?

一个简单的定义,如果一个对象有 length 属性值,则它就是类数组

那常见的类数组有哪些呢?

这在 DOM 中甚为常见,如各种元素检索 API 返回的都是类数组,如 document.getElementsByTagName,document.querySelectorAll 等等。除了 DOM API 中,常见的 function 中的 arguments 也是类数组

那如何把类数组转化为数组呢?这是类数组操作时一个典型的场景,也是一个典型的面试题

以下我们将以 { length: 3 } 来指代类数组,来作为演示

ES6+​

ES6 中有现成的 API:Array.from,极为简单

// [undefined, undefined, undefined]
Array.from({ length: 3 });

除了 Array.from 还有更简单的运算符 ... 扩展运算符,不过它只能作用于 iterable 对象,即拥有 Symbol(Symbol.iterator) 属性值

拥有 Symbol(Symbol.iterator) 属性值,意味着可以使用 for of 来循环迭代

// 适用于 iterable 对象
[...document.querySelectorAll("div")];

但是严格意义上来说,它不能把类数组转化为数组,如 { length: 3 }。它将会抛出异常

// Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
[...{ length: 3 }];
ES5​

在此之前,我们先不使用 { length: 3 },使用以下数据来代表类数组

const arrayLike = {  0: 3,  1: 4,  2: 5,  length: 3,};

在 ES5 中可以借用 Array API 通过 call/apply 改变 this 或者 arguments 来完成转化。

最常见的转换是 Array.prototype.slice

Array.prototype.slice.call(arrayLike);

当然由于借用 Array API,一切以数组为输入,并以数组为输出的 API 都可以来做数组转换,如

  • Array (借用 arguments)
  • Array.prototype.concat (借用 arguments)
  • Array.prototype.slice (借用 this)
  • Array.prototype.map (借用 this)
  • Array.prototype.filter (借用 this)
Array.apply(null, arrayLike);
Array.prototype.concat.apply([], arrayLike);
Array.prototype.slice.call(arrayLike);
Array.prototype.map.call(arrayLike, (x) => x);
Array.prototype.filter.call(arrayLike, (x) => 1);

此时一切正常,但是忘了一个特例,稀疏数组。在此之前,先做一个题,以下代码输出多少

// 该代码输出多少
Array(100).map((x) => 1);

参考 Array(100).map(x => 1) 结果是多少(opens in a new tab)

稀疏数组 (sparse array)​

使用 Array(n) 将会创建一个稀疏数组,为了节省空间,稀疏数组内含非真实元素,在控制台上将以 empty 显示,如下所示

[,,,] 与 Array(3) 都将返回稀疏数组

> [,,,]
[empty × 3]
> Array(3)
[empty × 3]

当类数组为 { length: 3 } 时,一切将类数组做为 this 的方法将都返回稀疏数组,而将类数组做为 arguments 的方法将都返回密集数组

总结​

由上总结,把类数组转化成数组最靠谱的方式是以下三个

Array.from(arrayLike);Array.apply(null, arrayLike);Array.prototype.concat.apply([], arrayLike);

以下几种方式需要考虑稀疏数组的转化

Array.prototype.filter.call(divs, (x) => 1);Array.prototype.map.call(arrayLike, (x) => x);Array.prototype.filter.call(arrayLike, (x) => 1);

以下方法要注意是否是 iterable object

[...arrayLike];

什么是稀疏数组?​

稀疏数组是指数组中包含空(undefined)或者未定义(empty)元素的数组。在 JavaScript 中,数组是一种特殊的对象,其属性名为 0、1、2 等连续的非负整数,称为数组的索引。稀疏数组是指这些索引中存在空缺的数组。

Array(100).map(x => 1) 结果是多少​

image-20240729151757186

生成了稀疏数组。

如果想要生成 100 个元素为 1 的数组:

Array.from(Array(100), (x) => 1);
Array.apply(null, Array(100)).map((x) => 1);
Array(100).fill(1);

如何判断数组或者类数组是否可迭代?​

要判断一个对象是否可迭代(iterable),可以检查对象是否具有一个名为 [Symbol.iterator] 的属性,且该属性是一个函数。在 JavaScript 中,可迭代对象是支持迭代协议的对象,可以通过 for...of 循环、Array.from() 等方法进行迭代。

对于数组或类数组对象,通常可以通过以下方式来判断其是否可迭代:

  1. 使用 [Symbol.iterator] 属性
function isIterable(obj) {
return typeof obj[Symbol.iterator] === 'function';
}

const arr = [1, 2, 3];
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 }; // 类数组对象

console.log(isIterable(arr)); // true
console.log(isIterable(arrayLike)); // false
  1. 使用 Array.isArray()

如果你想确认对象是一个数组,可以使用 Array.isArray() 方法,数组是可迭代的。

function isIterableArray(obj) {
return Array.isArray(obj);
}

console.log(isIterableArray(arr)); // true
console.log(isIterableArray(arrayLike)); // false
  1. 使用 Symbol.iterator in 对象
function isIterable(obj) {
return Symbol.iterator in obj;
}

console.log(isIterable(arr)); // true
console.log(isIterable(arrayLike)); // false

注意事项:​

  • 类数组对象通常具有 length 属性和以数字为键的属性,但它们并不一定是可迭代的。
  • ES6 中引入了符号 Symbol.iterator,它是 JavaScript 的内置符号,用于定义对象的默认迭代器。

通过上述方法,你可以判断一个对象(包括数组或类数组对象)是否可迭代,从而根据需要在代码中进行相应处理。

什么是 TypedArray​

TypedArray 是 JavaScript 中用来表示原始二进制数据的一组特定类型化数组(typed array)。这些数组提供了一种类似于 C 语言中数组的方式来操作二进制数据,可以存储特定类型的数据,并且支持高效的数据处理和操作。

常见的 TypedArray 类型:​
  • Int8Array: 8 位有符号整数数组
  • Uint8Array: 8 位无符号整数数组
  • Int16Array: 16 位有符号整数数组
  • Uint16Array: 16 位无符号整数数组
  • Int32Array: 32 位有符号整数数组
  • Uint32Array: 32 位无符号整数数组
  • Float32Array: 32 位浮点数数组
  • Float64Array: 64 位浮点数数组

什么是 Proxy 和 Reflect?有什么作用?​

Proxy 是用于创建一个代理对象,可以用来拦截并定义基本操作的自定义行为。通过 Proxy,可以定义各种操作的行为,比如属性查找、赋值、函数调用等,在目标对象上设置拦截器,实现对目标对象的访问控制和修改。

Reflect 是一个内置的对象,提供了一组静态方法,这些方法与目标对象上的原生方法相对应。Reflect 方法和对应的目标对象方法一一对应,让操作变得更加标准化、易读,并且使得元编程中的一些操作更容易实现。

简单来说,我们可以通过 Proxy 创建对于原始对象的代理对象,从而在代理对象中使用 Reflect 达到对于 JavaScript 原始操作的拦截。

为什么一定需要 Reflect 来配合 Proxy 呢?​

单独使用 Proxy​

开始的第一个例子,我们先单独使用 Proxy 来烹饪一道简单的开胃小菜:

const obj = {
name: 'wang.haoyu',
};

const proxy = new Proxy(obj, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key) {
console.log('劫持你的数据访问' + key);
return target[key]
},
});

proxy.name // 劫持你的数据访问name -> wang.haoyu

访问我们访问 proxy.name 时实际触发了对应的 get 陷阱,它会执行 get 陷阱中的逻辑,同时会执行对应陷阱中的逻辑,最终返回对应的 target[key] 也就是所谓的 wang.haoyu .

Proxy 中的 receiver​

上边的 Demo 中一切都看起来顺风顺水没错吧,细心的同学在阅读 Proxy 的 MDN 文档上可能会发现其实 Proxy 中 get 陷阱中还会存在一个额外的参数 receiver 。

那么这里的 receiver 究竟表示什么意思呢?大多数同学会将它理解成为代理对象,但这是不全面的。

接下来同样让我们以一个简单的例子来作为切入点:

const obj = {
name: 'wang.haoyu',
};

const proxy = new Proxy(obj, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
console.log(receiver === proxy);
return target[key];
},
});

// log: true
proxy.name;

上述的例子中,我们在 Proxy 实例对象的 get 陷阱上接收了 receiver 这个参数。

同时,我们在陷阱内部打印 console.log(receiver === proxy); 它会打印出 true ,表示这里 receiver 的确是和代理对象相等的。

所以 receiver 的确是可以表示代理对象,但是这仅仅是 receiver 代表的一种情况而已。

接下来我们来看另外一个例子:

const parent = {
get value() {
return '19Qingfeng';
},
};

const proxy = new Proxy(parent, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
console.log(receiver === proxy);
return target[key];
},
});

const obj = {
name: 'wang.haoyu',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
obj.value

我们可以看到,上述的代码同样我在 proxy 对象的 get 陷阱上打印了 console.log(receiver === proxy); 结果却是 false 。

那么你可以稍微思考下这里的 receiver 究竟是什么呢? 其实这也是 proxy 中 get 陷阱第三个 receiver 存在的意义。

它是为了传递正确的调用者指向,你可以看看下方的代码:

...
const proxy = new Proxy(parent, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
- console.log(receiver === proxy) // log:false
+ console.log(receiver === obj) // log:true
return target[key];
},
});
...

其实简单来说,get 陷阱中的 receiver 存在的意义就是为了正确的在陷阱中传递上下文。

涉及到属性访问时,不要忘记 get 陷阱还会触发对应的属性访问器,也就是所谓的 get 访问器方法。

我们可以清楚的看到上述的 receiver 代表的是继承与 Proxy 的对象,也就是 obj。

看到这里,我们明白了 Proxy 中 get 陷阱的 receiver 不仅仅代表的是 Proxy 代理对象本身,同时也许他会代表继承 Proxy 的那个对象。

其实本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问,比如这里的 receiver 指向的是 obj 。

当然,你不要将 revceiver 和 get 陷阱中的 this 弄混了,陷阱中的 this 关键字表示的是代理的 handler 对象。

比如:

const parent = {
get value() {
return '19Qingfeng';
},
};

const handler = {
get(target, key, receiver) {
console.log(this === handler); // log: true
console.log(receiver === obj); // log: true
return target[key];
},
};

const proxy = new Proxy(parent, handler);

const obj = {
name: 'wang.haoyu',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
obj.value

Reflect 中的 receiver​

在清楚了 Proxy 中 get 陷阱的 receiver 后,趁热打铁我们来聊聊 Reflect 反射 API 中 get 陷阱的 receiver。

我们知道在 Proxy 中(以下我们都以 get 陷阱为例)第三个参数 receiver 代表的是代理对象本身或者继承与代理对象的对象,它表示触发陷阱时正确的上下文。

const parent = {
name: '19Qingfeng',
get value() {
return this.name;
},
};

const handler = {
get(target, key, receiver) {
return Reflect.get(target, key);
// 这里相当于 return target[key]
},
};

const proxy = new Proxy(parent, handler);

const obj = {
name: 'wang.haoyu',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
console.log(obj.value);

我们稍微分析下上边的代码:

  • 当我们调用 obj.value 时,由于 obj 本身不存在 value 属性。
  • 它继承的 proxy 对象中存在 value 的属性访问操作符,所以会发生屏蔽效果。
  • 此时会触发 proxy 上的 get value() 属性访问操作。
  • 同时由于访问了 proxy 上的 value 属性访问器,所以此时会触发 get 陷阱。
  • 进入陷阱时,target 为源对象也就是 parent ,key 为 value 。
  • 陷阱中返回 Reflect.get(target,key) 相当于 target[key]。
  • 此时,不知不觉中 this 指向在 get 陷阱中被偷偷修改掉了!!
  • 原本调用方的 obj 在陷阱中被修改成为了对应的 target 也就是 parent 。
  • 自然而然打印出了对应的 parent[value] 也就是 19Qingfeng 。

这显然不是我们期望的结果,当我访问 obj.value 时,我希望应该正确输出对应的自身上的 name 属性也就是所谓的 obj.value => wang.haoyu 。

那么,Relfect 中 get 陷阱的 receiver 就大显神通了。

const parent = {
name: '19Qingfeng',
get value() {
return this.name;
},
};

const handler = {
get(target, key, receiver) {
- return Reflect.get(target, key);
+ return Reflect.get(target, key, receiver);
},
};

const proxy = new Proxy(parent, handler);

const obj = {
name: 'wang.haoyu',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: wang.haoyu
console.log(obj.value);

上述代码原理其实非常简单:

  • 首先,之前我们提到过在 Proxy 中 get 陷阱的 receiver 不仅仅会表示代理对象本身同时也还有可能表示继承于代理对象的对象,具体需要区别与调用方。这里显然它是指向继承与代理对象的 obj 。
  • 其次,我们在 Reflect 中 get 陷阱中第三个参数传递了 Proxy 中的 receiver 也就是 obj 作为形参,它会修改调用时的 this 指向。

你可以简单的将 Reflect.get(target, key, receiver) 理解成为 target[key].call(receiver),不过这是一段伪代码,但是这样你可能更好理解。

相信看到这里你已经明白 Relfect 中的 receiver 代表的含义是什么了,没错它正是可以修改属性访问中的 this 指向为传入的 receiver 对象。

总结​

相信看到这里大家都已经明白了,为什么 Proxy 一定要配合 Reflect 使用。恰恰是为什么触发代理对象的劫持时保证正确的 this 上下文指向。

我们再来稍稍回忆一下,针对于 get 陷阱(当然 set 其他之类涉及到 receiver 的陷阱同理):

  • Proxy 中接受的 Receiver 形参表示代理对象本身或者继承与代理对象的对象。
  • Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。

再加个备注,proxy 内如 get 函数的第一个参数 target 就是 new Proxy(target,handler)这个 target

JS 中实现继承的几种方式?​

ES5 和 ES6 实现继承有哪些区别?super 关键字有什么作用?​

介绍一下观察者模式和发布订阅模式的区别?​

介绍一下 JS 的事件执行机制?​

什么是事件委托?有什么作用?​

JS 实现异步有哪些方式?​

Promise 构造函数是同步还是异步的?then 方法呢?​

Promise 有哪些方法?分别有什么作用?​

什么是 Symbol?如果 Symbol 的 key 相同,那么两个 Symbol 一样吗?​

isNaN 和 Number.isNaN 一样吗?​

Async、Await、Generator、Promise 有什么区别和联系?​

Async 和 Await 是如何实现异步的?​

JS 中的垃圾回收机制?什么是新生代存储和老生代存储?​

Object.defineProperty 和 Proxy 有什么区别?​

JS 中的数组是如何存储的?​

什么情况下会导致内存泄漏?​

三种事件模型是什么?​

JS 中的各种宽高?如 clientHeight、scrollHeight、offsetHeight 的区别?​

前端如何实现跨域?有哪些方法?​

什么是 Web Worker?有什么用?​

什么是 Service Worker?有什么作用?​

浏览器的缓存机制?强缓存和协商缓存?​

CSS 加载会阻塞 DOM 渲染吗?​

什么是函数柯里化?​

JS 中有哪些隐式类型转换?​

JS 中变量的存储方式?​

this 的指向问题?​

说一下 V8 引擎原理?​

什么是 weakset 和 weakmap?应用场景?​

ES6、ES7、ES8...新特性有哪些?​

数组有哪些方法?分别有哪些参数?​

Object 有哪些方法?分别有那些参数?​

在 js 中如何把类数组转化为数组​

https://q.shanyue.tech/fe/js/169

Array.from()第二个参数有用过吗?​

什么是 Iterable 对象,与 Array 有什么区别​

https://q.shanyue.tech/fe/js/358

字符串的 reaplace 方法和 replaceAll 区别​

以下输出顺序多少 (setTimeout 与 promise 顺序)​

https://q.shanyue.tech/fe/js/396

如何实现一个函数 isPlainObject 判断是否为纯对象​

https://q.shanyue.tech/fe/js/409

什么是纯对象​

简单介绍以下浏览器中的 module​

<script type="module">

什么是 commonjs2​

介绍一下 bigint​

介绍 generator 和 yeild​

Map 与 WeakMap 有何区别​

https://q.shanyue.tech/fe/js/542

Javascript 数组中有那些方法可以改变自身,那些不可以​

不改变自身的:

slice map filter some find reduce concat join foreach findeIndex

改变自身的:

splice push shift pop unshift sort reverse

如何统计当前页面出现的所有标签​

https://q.shanyue.tech/fe/js/573

什么是媒体查询,JS 可以监听媒体查询吗​

https://q.shanyue.tech/fe/js/578

隐式类型转换有哪些?​

Object.is 与全等运算符(===)有何区别​

Number.isNaN 与 globalThis.isNaN 有何区别​

如何把字符串全部转化为小写格式​

https://q.shanyue.tech/fe/js/656

使用 JS 如何生成一个随机字符串​

https://q.shanyue.tech/fe/js/637

如何遍历一个对象​

https://q.shanyue.tech/fe/js/704

setTimeout 为什么最小只能设置 4ms,如何实现一个 0ms 的 setTimeout?​

https://q.shanyue.tech/fe/js/708

  1. 为什么 setTimeout 有最小时延 4ms ?(opens in a new tab)

  2. 如何实现一个 0ms 的 setTimeout?(opens in a new tab)

在 ES6 Class 中,super 的过程中做了什么​

https://q.shanyue.tech/fe/js/725

关于 Promise,判断以下代码的输出​

https://q.shanyue.tech/fe/js/727

Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then((res) => {
console.log(res);
});

Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});