ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

原生JavaScript核心概念解析:作用域、原型、异步与事件循环

原生JavaScript核心概念解析:作用域、原型、异步与事件循环 1. 为什么原生JS面试题是前端能力的试金石最近在帮团队面试前端同学也和一些资深面试官交流发现一个挺有意思的现象很多候选人谈起Vue、React的响应式原理、虚拟DOM、Hooks这些框架层面的东西头头是道但一碰到几道看似基础的原生JavaScript题目思路就开始卡壳甚至暴露出对语言本身理解的偏差。这让我想起自己刚入行时一位前辈说的话“框架是招式JS内功才是根基。招式可以速成内功却需要日积月累。” 这句话到现在依然适用。所谓“原生JS面试题”考察的正是这份“内功”。它不问你如何用Vue的v-for渲染列表而是问你for...in和for...of遍历数组和对象时究竟有何不同不问你React的useEffect怎么用而是问你闭包在循环中创建函数时为什么会出问题以及如何解决。这些问题直指JavaScript语言的核心机制作用域与闭包、原型与继承、事件循环、类型系统、异步编程等。掌握它们意味着你能真正理解你写的代码在底层是如何运行的而不仅仅是记住某个API的调用方式。这份整理了1-30题的清单正是围绕这些核心内功展开的。它不是为了刁难人而是帮你系统地检验和巩固对JavaScript语言本身的理解。无论你是正在准备面试的求职者还是希望夯实基础、突破瓶颈的在职开发者静下心来把这些题目搞懂、吃透都会让你对代码的掌控力上一个台阶。你会发现之前很多靠“背”下来的框架“黑魔法”其底层原理其实就藏在这些基础概念里。2. 作用域、闭包与this指向代码执行环境的底层逻辑这是JavaScript中最容易混淆也最常被考察的三个核心概念。它们共同决定了变量在哪里被定义、函数如何访问外部变量以及函数内部的this关键字到底指向谁。2.1 作用域链与变量查找机制JavaScript的作用域是词法作用域静态作用域意味着函数的作用域在函数定义时就决定了而不是在调用时。当访问一个变量时引擎会从当前作用域开始查找如果没找到就向外层作用域继续查找直到全局作用域形成一条“作用域链”。var globalVar global; function outer() { var outerVar outer; function inner() { var innerVar inner; console.log(innerVar); // 从当前作用域找到 inner console.log(outerVar); // 从外层outer作用域找到 outer console.log(globalVar); // 从更外层的全局作用域找到 global console.log(notDefined); // 报错ReferenceError 链上找不到 } inner(); } outer();一个经典的面试题变形下面代码的输出是什么for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 1000); }答案是连续输出5个5。因为var声明的i属于函数作用域在这个例子中是全局作用域循环结束后i的值已经是5。setTimeout中的回调函数在1秒后执行它们共享同一个外部作用域中的变量i所以都打印5。这引出了闭包和块级作用域的重要性。2.2 闭包函数与其词法环境的捆绑闭包是指一个函数能够记住并访问其词法作用域即使该函数在其词法作用域之外执行。它是实现数据私有化、模块化和函数柯里化的基础。利用闭包可以解决上面的循环问题for (var i 0; i 5; i) { (function(j) { // 立即执行函数创建了一个新的作用域参数j捕获了当前循环的i值 setTimeout(function() { console.log(j); // 0, 1, 2, 3, 4 }, 1000); })(i); }更现代的写法是使用let声明变量它拥有块级作用域每次循环都会创建一个新的块级作用域本质上也是为每次迭代创建了一个新的“闭包环境”for (let i 0; i 5; i) { setTimeout(function() { console.log(i); // 0, 1, 2, 3, 4 }, 1000); }闭包的实际应用与内存管理闭包会导致外部函数的变量无法被垃圾回收因为内部函数持有对其的引用。这是一个双刃剑在需要持久化状态时很有用如计数器、缓存但也可能引起内存泄漏。例如将DOM元素引用存储在闭包中即使该DOM元素已从页面移除只要闭包存在它就无法被回收。2.3this的动态绑定规则this的指向是动态的在函数被调用时才确定。主要有四种绑定规则默认绑定独立函数调用。非严格模式下this指向全局对象浏览器中是window严格模式下为undefined。function foo() { console.log(this.a); } var a 2; foo(); // 2 (非严格模式)隐式绑定函数作为对象的方法被调用。this指向调用该方法的对象。var obj { a: 2, foo: function() { console.log(this.a); } }; obj.foo(); // 2隐式丢失是一个常见陷阱将对象方法赋值给一个变量再调用会应用默认绑定规则。var bar obj.foo; bar(); // undefined (严格模式下) 或 2 (非严格模式指向windowwindow.a未定义)显式绑定使用call、apply、bind方法强制指定this。function foo() { console.log(this.a); } var obj { a: 2 }; foo.call(obj); // 2 var boundFoo foo.bind(obj); boundFoo(); // 2new绑定使用new操作符调用构造函数。this会绑定到新创建的对象实例上。function Foo(a) { this.a a; } var bar new Foo(2); console.log(bar.a); // 2优先级new绑定 显式绑定 隐式绑定 默认绑定。箭头函数的this箭头函数没有自己的this它的this继承自外层第一个普通函数的this且一旦确定就不可更改call、apply、bind也无法改变。这使得箭头函数在需要固定this指向的场景如回调函数中非常有用但也因此不能作为构造函数。3. 原型、继承与面向对象JavaScript的“类”系统JavaScript是一门基于原型的语言它的“继承”机制与基于类的语言如Java有本质不同。理解原型链是理解JavaScript对象系统的关键。3.1 构造函数、原型对象与实例的关系每个函数都有一个prototype属性原型对象每个对象都有一个__proto__属性指向其构造函数的原型对象现已建议使用Object.getPrototypeOf()获取。function Person(name) { this.name name; } // 在Person的原型上添加方法所有实例共享 Person.prototype.sayHello function() { console.log(Hello, this.name); }; var person1 new Person(Alice); var person2 new Person(Bob); person1.sayHello(); // Hello, Alice person2.sayHello(); // Hello, Bob console.log(person1.__proto__ Person.prototype); // true console.log(Person.prototype.constructor Person); // true console.log(person1 instanceof Person); // true关系图概念示意实例 (person1) ├── __proto__ -- Person.prototype └── 自身属性 (name: Alice) Person.prototype ├── constructor -- Person函数 └── 共享方法 (sayHello) Person函数 └── prototype -- Person.prototype3.2 原型链与属性查找当访问一个对象的属性时如果对象自身没有引擎会沿着它的__proto__即其构造函数的prototype向上查找直到找到该属性或到达原型链的尽头null。这就是原型链。console.log(person1.hasOwnProperty(sayHello)); // false sayHello来自原型 console.log(person1.hasOwnProperty(name)); // true // 查找过程person1自身 - Person.prototype - Object.prototype - null console.log(person1.toString()); // [object Object] 来自Object.prototype3.3 实现继承的几种模式原型链继承让子类的原型等于父类的实例。function Parent() { this.names [kevin, daisy]; } function Child() {} Child.prototype new Parent(); // 关键 var child1 new Child(); child1.names.push(yayu); var child2 new Child(); console.log(child2.names); // [kevin, daisy, yayu] 问题引用类型属性被所有实例共享构造函数继承在子类构造函数内部调用父类构造函数。function Parent(name) { this.name name; this.colors [red, blue]; } function Child(name) { Parent.call(this, name); // 关键 } var child1 new Child(child1); child1.colors.push(green); var child2 new Child(child2); console.log(child2.colors); // [red, blue] 优点解决了引用属性共享问题。 console.log(child2.name); // child2 // 缺点方法都在构造函数中定义无法复用。组合继承最常用结合原型链继承和构造函数继承。function Parent(name) { this.name name; this.colors [red, blue]; } Parent.prototype.sayName function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); // 第二次调用Parent this.age age; } Child.prototype new Parent(); // 第一次调用Parent Child.prototype.constructor Child; // 修正constructor指向 Child.prototype.sayAge function() { console.log(this.age); }; var child1 new Child(kevin, 18); child1.colors.push(green); console.log(child1.colors); // [red, blue, green] child1.sayName(); // kevin child1.sayAge(); // 18 var child2 new Child(daisy, 20); console.log(child2.colors); // [red, blue] // 缺点调用了两次父类构造函数。寄生组合式继承理想方式解决组合继承调用两次构造函数的问题。function inheritPrototype(child, parent) { var prototype Object.create(parent.prototype); // 创建父类原型副本 prototype.constructor child; // 增强对象 child.prototype prototype; // 指定对象 } function Parent(name) { this.name name; } Parent.prototype.sayName function() { /* ... */ }; function Child(name, age) { Parent.call(this, name); // 只调用一次 this.age age; } inheritPrototype(Child, Parent); Child.prototype.sayAge function() { /* ... */ };这是ES5时代最理想的继承方式。ES6的class和extends语法糖其底层实现原理就是寄生组合式继承。3.4 ES6 Class 语法糖class让面向对象编程的语法更清晰但它本质上仍然是基于原型的。class Parent { constructor(name) { this.name name; } sayName() { console.log(this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 相当于 Parent.call(this, name) this.age age; } sayAge() { console.log(this.age); } } const child new Child(Alice, 10); child.sayName(); // Alice console.log(child instanceof Parent); // true注意class中的方法是定义在原型上的且默认是严格模式。super关键字在构造函数中必须在使用this之前调用。4. 异步编程与事件循环从回调地狱到优雅处理JavaScript是单线程的异步编程是其核心特性用于处理I/O、网络请求、定时等不阻塞主线程的任务。理解事件循环Event Loop是理解异步代码执行顺序的关键。4.1 回调函数与“回调地狱”最初的异步模式是回调函数。// 模拟一个异步读取文件的操作 function readFile(path, callback) { setTimeout(() { console.log(读取 ${path} 完成); callback(null, 来自 ${path} 的数据); }, Math.random() * 1000); } // 回调地狱多层嵌套难以阅读和维护 readFile(/etc/passwd, function(err, data) { if (err) throw err; console.log(data); readFile(/etc/hosts, function(err, data2) { if (err) throw err; console.log(data2); readFile(/etc/profile, function(err, data3) { if (err) throw err; console.log(data3); // ... 更多嵌套 }); }); });4.2 Promise更优雅的异步管理Promise对象代表一个异步操作的最终完成或失败及其结果值。它有三种状态pending进行中、fulfilled已成功、rejected已失败。const promise new Promise((resolve, reject) { // 异步操作 setTimeout(() { const success Math.random() 0.5; if (success) { resolve(操作成功); } else { reject(new Error(操作失败)); } }, 1000); }); promise .then((result) { console.log(成功, result); return result 附加信息; // 可以返回新值或新的Promise }) .then((newResult) { console.log(链式调用, newResult); }) .catch((error) { console.error(失败, error); }) .finally(() { console.log(无论成功失败都会执行); });Promise的静态方法Promise.all(iterable)所有Promise都成功时返回结果数组任何一个失败则立即失败。Promise.race(iterable)第一个 settled成功或失败的Promise的结果决定最终结果。Promise.allSettled(iterable)所有Promise都 settled 后返回一个描述每个结果的对象数组。Promise.any(iterable)任何一个成功即成功全部失败才失败。4.3 Generator 与 Async/AwaitGenerator函数可以暂停执行和恢复执行是协程在ES6的实现。async/await是Generator函数的语法糖使得异步代码看起来像同步代码。// Generator 示例 function* gen() { yield hello; yield world; return ending; } const g gen(); console.log(g.next()); // { value: hello, done: false } console.log(g.next()); // { value: world, done: false } console.log(g.next()); // { value: ending, done: true } // 用 Generator Promise 模拟 async/await function readFilePromise(path) { return new Promise(resolve { setTimeout(() resolve(数据来自 ${path}), 1000); }); } function* readFiles() { const data1 yield readFilePromise(file1); console.log(data1); const data2 yield readFilePromise(file2); console.log(data2); } // 需要一个执行器来运行 function run(gen) { const g gen(); function next(data) { const result g.next(data); if (result.done) return result.value; result.value.then(data next(data)); } next(); } run(readFiles); // Async/Await (最终方案) async function readFilesAsync() { try { const data1 await readFilePromise(file1); console.log(data1); const data2 await readFilePromise(file2); console.log(data2); } catch (error) { console.error(error); } } readFilesAsync();async函数返回一个Promiseawait后面通常跟一个Promise它会暂停async函数的执行等待Promise settled然后恢复执行并返回结果。4.4 事件循环Event Loop机制详解这是面试中的高频难点。JavaScript运行时包含一个调用栈Call Stack、一个微任务队列Microtask Queue和一个宏任务队列Macrotask Queue也叫Task Queue。执行顺序规则执行全局同步代码这算一个宏任务。执行过程中遇到异步任务根据类型分发Promise.then/catch/finally、MutationObserver、queueMicrotask产生的任务进入微任务队列。setTimeout、setInterval、setImmediate(Node)、I/O操作、UI渲染等产生的任务进入宏任务队列。当前宏任务即调用栈清空后立即检查并清空整个微任务队列如果执行微任务时又产生了新的微任务会继续执行直到微任务队列为空。执行下一个宏任务从宏任务队列中取一个。循环步骤3-4。经典面试题说出下面代码的输出顺序。console.log(1); setTimeout(function() { console.log(2); new Promise(function(resolve) { console.log(3); resolve(); }).then(function() { console.log(4); }); }, 0); new Promise(function(resolve) { console.log(5); resolve(); }).then(function() { console.log(6); }); console.log(7);分析过程同步代码输出1,5,7。同时setTimeout回调宏任务入队Promise.then微任务入队。当前宏任务主线程执行完毕清空微任务队列输出6。取下一个宏任务setTimeout回调执行输出2,3。执行中又产生了一个新的微任务Promise.then。该宏任务执行完毕清空微任务队列输出4。最终输出顺序1, 5, 7, 6, 2, 3, 4。注意async函数中await之前的代码是同步执行的await表达式本身会阻塞其后的代码但await后面的Promise被放入微任务队列。可以简单理解为await fn()等价于Promise.resolve(fn()).then(...)。5. 类型、运算与内置对象那些容易踩的坑JavaScript是动态弱类型语言其类型转换和运算规则常常出人意料也是面试官喜欢考察的点。5.1 数据类型与类型判断七种原始类型Undefined,Null,Boolean,Number,String,Symbol(ES6),BigInt(ES2020)。一种引用类型Object包括Array,Function,Date,RegExp等。类型判断方法typeof能区分大部分原始类型但typeof null object历史遗留bugtypeof函数返回function。instanceof检测构造函数的prototype属性是否出现在对象的原型链上。用于检测自定义对象或内置对象。Object.prototype.toString.call()最准确的类型判断方法。Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(new Date()); // [object Date]Array.isArray()专门判断数组。5.2 隐式类型转换强制类型转换这是坑最多的地方发生在运算符、等号、逻辑判断等场景。与严格相等类型和值都必须相同。抽象相等如果类型不同会先进行隐式类型转换再比较。 false // true (都转为数字0) [] false // true ([]转为字符串再转为数字0false转为数字0) null undefined // true null 0 // false (null只与undefined相等) 0 false // true运算符优先进行字符串连接。1 1 // 11 1 true // 2 (true转为1) 1 undefined // NaN (undefined转为NaN) 1 null // 1 (null转为0) [] [] // (数组转为字符串) [] {} // [object Object] ([]转为 {}转为[object Object]) {} [] // 0 (这里的{}被解析为代码块 [] 将[]转为数字0)其他运算符通常将操作数转为数字。5 - 2 // 3 5 * 2 // 10 true - false // 1 abc - 1 // NaN逻辑判断中的转换if,,||,!会将条件转为布尔值。假值Falsy包括false,0,-0,0n,,null,undefined,NaN。其余均为真值Truthy。5.3 数组与对象的常用API与陷阱数组方法会改变原数组push/pop/shift/unshift,splice,sort,reverse,fill,copyWithin。不改变原数组slice,concat,map,filter,reduce,forEach,find,some,every等。一个关于sort的坑[10, 5, 40, 25].sort(); // [10, 25, 40, 5] 默认按字符串Unicode码点排序 [10, 5, 40, 25].sort((a, b) a - b); // [5, 10, 25, 40] 正确排序对象遍历for...in遍历对象自身和继承的可枚举字符串键属性。通常需要配合hasOwnProperty检查。const obj { a: 1, b: 2 }; for (let key in obj) { if (obj.hasOwnProperty(key)) { console.log(key, obj[key]); } }Object.keys()返回对象自身可枚举属性的键名数组。Object.values()返回对象自身可枚举属性的值数组。Object.entries()返回对象自身可枚举属性的键值对数组。for...of与for...in的区别for...of用于遍历可迭代对象如Array, Map, Set, String, arguments等的值。for...in用于遍历对象的键名。const arr [a, b]; arr.customProp hi; for (let i in arr) { console.log(i); } // 0, 1, customProp for (let v of arr) { console.log(v); } // a, b5.4 深拷贝与浅拷贝浅拷贝只拷贝对象的第一层属性。如果属性是引用类型拷贝的是引用地址新旧对象会共享这个引用。// 浅拷贝方法 const obj { a: 1, b: { c: 2 } }; const shallowCopy1 Object.assign({}, obj); const shallowCopy2 { ...obj }; // 扩展运算符 shallowCopy1.b.c 99; console.log(obj.b.c); // 99 原对象也被修改了深拷贝完全拷贝一个对象包括其嵌套的引用类型新旧对象完全独立。// 简单深拷贝有局限 const deepCopySimple JSON.parse(JSON.stringify(obj)); // 局限无法处理函数、Symbol、undefined、循环引用会忽略这些属性或报错。 // 手写一个基础深拷贝函数递归处理循环引用需额外逻辑 function deepClone(source, map new WeakMap()) { if (source null || typeof source ! object) { return source; // 原始类型直接返回 } if (map.has(source)) { return map.get(source); // 解决循环引用 } const target Array.isArray(source) ? [] : {}; map.set(source, target); for (let key in source) { if (source.hasOwnProperty(key)) { target[key] deepClone(source[key], map); } } return target; } // 注意这个基础版本未处理Date、RegExp、Function等特殊对象生产环境建议使用lodash的_.cloneDeep。6. DOM、BOM与事件浏览器环境的交互核心前端开发离不开与浏览器文档对象模型DOM和浏览器对象模型BOM的交互。6.1 DOM操作与性能优化获取DOM元素是高频操作选择合适的方法很重要。// 获取单个元素 document.getElementById(id); // 最快 document.querySelector(#id .class); // 灵活但相对慢 // 获取元素集合返回NodeList或HTMLCollection是类数组非真正的数组 document.getElementsByClassName(class); // 动态集合 document.getElementsByTagName(div); document.querySelectorAll(.class); // 静态集合推荐 // 创建与插入 const div document.createElement(div); div.textContent Hello; div.className box; // 插入操作 parent.appendChild(div); // 末尾插入 parent.insertBefore(newNode, referenceNode); // 指定位置前插入 parent.replaceChild(newChild, oldChild); // 替换性能优化要点减少DOM访问将频繁访问的DOM元素存储在变量中。批量操作使用DocumentFragment或先脱离文档流操作再一次性插入。const fragment document.createDocumentFragment(); for (let i 0; i 1000; i) { const li document.createElement(li); li.textContent Item ${i}; fragment.appendChild(li); } document.getElementById(list).appendChild(fragment); // 一次重排重绘避免触发同步布局布局抖动不要在循环中交替读取和修改样式这会导致浏览器反复计算布局。// 不好读写交替 for (let i 0; i boxes.length; i) { boxes[i].style.width boxes[i].offsetWidth 10 px; // 读 offsetWidth 触发重排 } // 好先读后写 const widths []; for (let i 0; i boxes.length; i) { widths[i] boxes[i].offsetWidth; } for (let i 0; i boxes.length; i) { boxes[i].style.width widths[i] 10 px; }6.2 事件流捕获与冒泡DOM事件流包括三个阶段捕获阶段从window向下到目标元素、目标阶段、冒泡阶段从目标元素向上到window。// addEventListener 第三个参数 element.addEventListener(click, handler, true); // 捕获阶段触发 element.addEventListener(click, handler, false); // 冒泡阶段触发默认 element.addEventListener(click, handler, { capture: true, once: true, passive: true }); // 选项对象事件委托利用事件冒泡将事件监听器设置在父元素上通过event.target来定位实际触发事件的子元素。这对于动态添加的子元素或大量子元素非常高效。ul idparent-list li>// 创建自定义事件 const myEvent new CustomEvent(myevent, { detail: { // 可以通过detail传递自定义数据 message: Hello from custom event!, time: new Date() }, bubbles: true, // 是否冒泡 cancelable: true // 是否可取消 }); // 监听自定义事件 const elem document.getElementById(myElement); elem.addEventListener(myevent, function(event) { console.log(Custom event received:, event.detail.message); }); // 触发自定义事件 elem.dispatchEvent(myEvent);6.4 BOM常用对象window浏览器窗口的全局对象。location包含当前URL信息。location.href,location.search,location.hash。history操作浏览器会话历史。history.back(),history.forward(),history.go(n),history.pushState(),history.replaceState()用于前端路由。navigator浏览器信息。navigator.userAgent,navigator.platform。screen屏幕信息。screen.width,screen.height。localStorage/sessionStorage本地存储。关于window.history.back()它等同于用户点击浏览器的后退按钮。在单页应用SPA中通常需要监听popstate事件来响应路由变化而不是直接依赖back()的同步行为。7. 模块化、ES6新特性与代码组织随着前端项目日益复杂模块化成为必备技能。ES6引入了官方的模块化标准。7.1 模块化演进从IIFE到ES ModuleIIFE立即执行函数表达式早期的模块化手段利用函数作用域创建私有空间。var Module (function() { var privateVar hidden; function privateMethod() { /* ... */ } return { publicMethod: function() { console.log(privateVar); } }; })(); Module.publicMethod(); // hidden // Module.privateVar // undefinedCommonJSNode.js的模块规范使用require导入module.exports导出。同步加载适用于服务器端。// math.js function add(a, b) { return a b; } module.exports { add }; // 或 exports.add add; // main.js const math require(./math.js); console.log(math.add(2, 3));AMD (Asynchronous Module Definition)异步模块定义代表是RequireJS。适用于浏览器端。// 定义模块 define([dependency], function(dependency) { return { method: function() { dependency.doSomething(); } }; }); // 加载模块 require([module], function(module) { module.method(); });ES Module (ESM)ES6语言标准模块系统。现代浏览器和Node.js需.mjs后缀或package.json中设置type: module原生支持。// math.mjs 或 math.js (在支持ESM的环境中) export function add(a, b) { return a b; } export const PI 3.14159; // 或 export default function add() { ... } // main.mjs import { add, PI } from ./math.mjs; // 或 import * as math from ./math.mjs; // 或 import add from ./math.mjs; (对应export default) console.log(add(2, 3), PI);ESM特性静态解析编译时确定依赖、异步加载、支持循环依赖、输出的是值的只读引用CommonJS输出的是值的拷贝。7.2 常用ES6新特性速览let/const块级作用域变量声明。const用于声明常量对于对象其引用不可变但属性可变。箭头函数(params) { expression }。无自己的this、arguments、super、new.target不能作为构造函数。模板字符串Hello, ${name}!支持多行字符串和嵌入表达式。解构赋值const [a, b] [1, 2]; const { name, age } person; const { data: userInfo } response; // 重命名扩展运算符与剩余参数const arr1 [1, 2]; const arr2 [...arr1, 3, 4]; // [1, 2, 3, 4] function sum(...numbers) { /* numbers是数组 */ } const { a, ...rest } obj; // 剩余属性默认参数function greet(name Guest) { }。MapSetconst map new Map(); map.set(key, value); map.get(key); const set new Set([1, 2, 2, 3]); // [1, 2, 3]Promiseasync/await前面已详述Symbol唯一的值常用于定义对象的唯一属性键。ProxyReflect元编程拦截并定义对象的基本操作。const handler { get(target, prop) { console.log(Getting ${prop}); return Reflect.get(...arguments); } }; const proxy new Proxy({a: 1}, handler); proxy.a; // 输出: Getting a可选链操作符?.obj?.prop如果obj是null或undefined则返回undefined而不是报错。空值合并运算符??a ?? b仅在a是null或undefined时返回b。BigInt用于表示任意精度的整数。const bigNum 9007199254740991n;7.3 代码组织与设计模式理解模块化后在组织代码时一些经典的设计模式能提升代码的可维护性和复用性。单例模式确保一个类只有一个实例。const Singleton (function() { let instance; function createInstance() { return { /* ... */ }; } return { getInstance: function() { if (!instance) { instance createInstance(); } return instance; } }; })(); const s1 Singleton.getInstance(); const s2 Singleton.getInstance(); console.log(s1 s2); // true观察者模式/发布-订阅模式对象间一对多的依赖关系当一个对象状态改变所有依赖它的对象都会得到通知。事件系统就是典型的观察者模式。工厂模式不暴露创建对象的具体逻辑而是用一个工厂方法来创建对象。策略模式定义一系列算法将它们封装起来并且使它们可以相互替换。在实际项目中结合ES Module和这些设计模式可以构建出清晰、松耦合、易于测试的代码结构。例如将不同的功能如网络请求、状态管理、工具函数拆分成独立的模块通过导入导出进行组合是现代前端工程化的基础。
返回列表