乐闻世界logo
搜索文章和话题

JavaScript面试题手册

script 标签的 defer 和 async 有什么区别?

当您在 HTML 文档中使用 <script> 标签引入 JavaScript 时,defer 和 async 属性可以控制脚本的加载和执行方式,它们之间的区别主要在于脚本加载的时间以及执行的时机。defer 属性使用 defer 属性的 <script> 标签会让脚本在文档解析期间异步下载,但是会延迟到整个文档解析完毕之后、DOMContentLoaded 事件触发之前执行。这意味着带有 defer 的脚本总是在文档解析完成之后执行,保证了执行时 DOM 已经完全构建好。例子:<script src="example.js" defer></script>如果您有多个带有 defer 属性的脚本,它们将按照在文档中出现的顺序执行,即便有些脚本可能会比其他脚本更早下载完成。async 属性而 async 属性也允许脚本在文档解析时异步下载,但是它一旦下载完成就会立即执行,这可能会在文档的其余部分尚未解析完毕时发生。因此,使用 async 的脚本不能保证按照在页面中出现的顺序执行,也无法保证 DOM 完全构建完成。例子:<script src="example.js" async></script>async 适用于那些不依赖于其他脚本且不依赖于 DOM 的脚本,例如,广告加载或者埋点脚本。总结defer 确保脚本在文档完全解析和 DOM 构建完成后,但在 DOMContentLoaded 事件之前执行。async 确保脚本在下载完成后尽快执行,但可能会打断文档的解析过程。没有这两个属性的 <script> 标签会立即下载并阻塞文档解析直到脚本执行完成。在实际应用中,选择 defer 或 async 取决于脚本对文档解析的依赖性,以及脚本之间的依赖关系。如果您需要确保脚本按照顺序执行,并且在 DOM 完全构建后执行,那么 defer 是更好的选择。如果脚本的执行顺序不重要,并且想尽快获取并执行脚本,可以使用 async。
阅读 47·2024年8月5日 04:52

javascript 中垃圾回收的方法有哪些?

JavaScript中的垃圾回收(garbage collection)是一种自动内存管理机制,它帮助开发者不需要手动释放分配的内存。在JavaScript中,垃圾回收主要采用了以下几种方法:1. 标记清除(Mark and Sweep)这是最常见的垃圾回收算法。当变量进入环境时,就“标记”这个变量为“进入环境”。当变量离开环境时,则“标记”这个变量为“离开环境”。垃圾收集器会定期运行,它会检查所有的变量,以及它们引用的其他变量是否还在环境中。如果一个变量已经不再环境中,且没有任何其他变量引用它,那么这个变量占用的内存就会被回收。例子:function processData() { var data = { /* 大量数据 */ }; // 使用data进行处理}processData();// processData执行完毕后,data变量离开环境,变成无法访问的状态,会被标记为可回收。2. 引用计数(Reference Counting)引用计数是另一种垃圾回收机制。在这个系统中,每一个值都有一个“引用数”,表示有多少变量或资源引用这个值。如果引用数变为0,则表示该值不再需要,其占用的内存可以被回收。这种方法的一个问题是循环引用:如果两个对象互相引用,即便它们已经不再需要,它们的引用数也不会降到0,导致内存无法被回收。例子:function referenceCycle() { var objectA = {}; var objectB = {}; objectA.other = objectB; objectB.other = objectA;}referenceCycle();// 即使referenceCycle函数执行结束,objectA和objectB因为相互引用,它们的引用数都不为0,造成内存泄漏。3. 分代收集(Generational Collection)分代收集是基于对象存活时间的假设,将对象分为两组:“新生代”和“老生代”。新创建的对象属于新生代,对象如果存活足够长的时间,就会被移动到老生代。通常新生代使用标记-复制(mark-copy)算法,老生代使用标记-清除(mark-sweep)或标记-整理(mark-compact)算法。4. 标记-整理(Mark-Compact)这种方法是对标记-清除的改进。在标记阶段,标记所有活动对象,然后在整理阶段,将所有活动的对象移动到内存的一端,然后清理掉边界之外的内存。5. 增量收集(Incremental Collection)增量收集是将垃圾回收分成小片段执行,每次只处理一部分对象,然后暂停,让程序执行。这种方式可以减少垃圾收集过程中的停顿时间。6. 闲时收集(Idle-time Collection)某些JavaScript引擎会利用CPU空闲时间来执行垃圾回收的工作,以避免影响到程序的执行效率。
阅读 38·2024年8月5日 04:52

JavaScript 中对象 instanceOf 属性的原理是什么?

instanceof 是 JavaScript 中的一个二元操作符,用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。当我们使用语法 object instanceof Constructor 时,instanceof 会沿着 object 的原型链向上查找,检查是否有原型对象等于 Constructor.prototype。如果找到了这样的原型,instanceof 就会返回 true;如果一直查找到原型链的顶端(也就是 null),依然没有找到,就会返回 false。原型链是 JavaScript 中实现继承的一种机制。每个对象都有一个内部链接指向另一个对象,即它的原型。那个原型对象也有自己的原型,以此类推,直到一个对象的原型为 null。常见的 Object.prototype 就是很多对象原型链的终点。这里举一个例子来说明 instanceof 的工作原理:function Car(make, model) { this.make = make; this.model = model;}const myCar = new Car('Toyota', 'Corolla');console.log(myCar instanceof Car); // 输出:trueconsole.log(myCar instanceof Object); // 输出:true在这个例子中,myCar instanceof Car 返回 true,因为 Car.prototype 在 myCar 的原型链上。myCar instanceof Object 也返回 true,尽管我们没有直接设置 myCar 的原型为 Object.prototype。但由于 JavaScript 中几乎所有对象的原型链的最终都会指向 Object.prototype,因此任何对象基本上都是 Object 的一个实例。总的来说,instanceof 操作符提供了一种检查对象类型的方法,并且能够识别对象继承关系中的原型。
阅读 11·2024年8月5日 04:52

JavaScript 中的原型&原型链是怎么工作的?

JavaScript中的原型(prototype)和原型链(prototype chain)是其面向对象编程的基础。这两个概念是理解JavaScript中对象之间的关系和继承机制非常关键的部分。原型(Prototype)在JavaScript中,每一个函数创建时都会有一个名为 prototype的属性,这个属性是一个对象,它包含了可以由特定类型的所有实例共享的属性和方法。这意味着你可以使用原型来添加或者分享功能,而不需要在每个实例中重新定义这些功能。例如,假设我们定义了一个构造函数:function Person(name) { this.name = name;}Person.prototype.sayName = function() { console.log(this.name);};以下是通过 Person构造函数创建的两个实例:var person1 = new Person("Alice");var person2 = new Person("Bob");这里的 person1和 person2都没有自己的 sayName方法。当你调用 person1.sayName()时,JavaScript会查找 person1是否有这个方法。由于 person1没有,它会通过原型链查找 Person.prototype是否有 sayName方法。这样,person1和 person2实际上共享了 Person.prototype上的 sayName方法。原型链(Prototype Chain)原型链是JavaScript实现继承的机制。每个对象都有一个内部链接到另一个对象,即它的原型。这个原型对象本身也有一个原型,以此类推,直到某个对象的原型为 null。按照规范,null的原型没有原型,这通常被视为原型链的末端。当你尝试访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript引擎会沿着原型链向上查找,直到找到这个属性或者到达原型链的末端。继续上面的例子,我们可以看到原型链是如何工作的:person1.sayName(); // 输出: Alice首先,JavaScript引擎检查 person1自身是否有 sayName属性。发现 person1没有,引擎接着检查 person1的原型(即 Person.prototype)。Person.prototype有 sayName方法,所以这个方法被调用。如果我们有一个对象的原型是另一个对象,那么第二个对象的属性和方法也可以被第一个对象访问。这个过程可以一直持续下去,形成了一个“链”。通过原型链实现继承我们可以使用原型链来实现继承。例如,如果我们有一个 Employee构造函数,它应该继承 Person:function Employee(name, title) { Person.call(this, name); // 继承属性 this.title = title;}Employee.prototype = Object.create(Person.prototype);Employee.prototype.constructor = Employee;Employee.prototype.sayTitle = function() { console.log(this.title);};var employee1 = new Employee("Charlie", "Developer");employee1.sayName(); // 输出: Charlieemployee1.sayTitle(); // 输出: Developer在这里,Employee.prototype被设置为一个新对象,这个新对象的原型是 Person.prototype。这样,Employee的所有实例都继承了 Person的方法。我们还修正了 Employee.prototype.constructor属性,确保它指向正确的构造函数。通过上面的代码,我们创建了一个 Employee的实例 employee1,它能够调用继承自 Person的 sayName方法,同时还有自己特有的 sayTitle方法。
阅读 18·2024年8月5日 04:52

什么是闭包?什么场景需要使用闭包?

什么是闭包?在计算机科学中,闭包(Closure)是指一个函数绑定了其外部作用域的变量,因此这个函数可以在其定义环境之外被调用时仍能访问到那些绑定的变量。简单来说,闭包让你可以从一个函数内部访问到其外部函数作用域的变量。闭包的特点是:函数嵌套:通常闭包包含一个函数内定义的另一个函数。环境捕获:内部函数会捕获定义它的外部函数的作用域中的变量。作用域链:内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。在 JavaScript 中,闭包是一种非常常见和强大的特性,因为 JavaScript 是词法作用域的语言,函数的作用域在函数定义时就已经确定了。什么场景需要使用闭包?闭包通常用于以下几种场景:数据封装和私有化:使用闭包可以创建私有变量,这些变量只能被特定的函数访问和修改,从而模拟出类似私有属性的效果。这在模块模式中尤其常见。例子:一个简单的计数器函数,利用闭包可以隐藏计数器的值,只能通过特定的函数来操作。 function createCounter() { let count = 0; return { increment: function() { count += 1; }, decrement: function() { count -= 1; }, getCount: function() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 输出 1回调函数:在异步编程中,闭包常用于回调函数中,以确保异步操作完成时能够访问到定义回调时的环境状态。例子:在一个异步请求中使用闭包记住请求开始时的状态。 function fetchData(url, callback) { // 假设这里发起了一个异步请求 setTimeout(() => { // 模拟异步操作 const data = 'fetched data'; // 假设这是响应数据 callback(data); }, 1000); } function requestData() { const requestTimestamp = Date.now(); fetchData('https://example.com/data', function(data) { console.log(`Request took ${Date.now() - requestTimestamp} ms`); console.log(`Data received: ${data}`); }); } requestData();函数工厂:闭包可以用来创建可以记住和操作环境状态的函数,这些函数根据不同的参数创建出来,具有不同的行为。例子:根据不同的倍数创建乘法函数。 function createMultiplier(multiplier) { return function(x) { return x * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 输出 10 console.log(triple(5)); // 输出 15节流和防抖:在 JavaScript 的 DOM 事件处理中,为了优化性能,防止过多的事件处理函数被频繁触发,会使用闭包来实现函数节流(throttle)和防抖(debounce)。例子:使用防抖确保事件处理函数在特定时间内只执行一次。
阅读 16·2024年8月5日 04:52

封装一个可以设置过期时间的localStorage存储函数

实现一个具有过期时间功能的localStorage存储函数,需要定义一个函数,它会将数据和过期时间一起存储在localStorage中。 下面是一个简单的实现示例:/** * 设置带过期时间的localStorage * @param {string} key - 存储的键名 * @param {*} value - 要存储的值,可以是任何可序列化的数据 * @param {number} ttl - 过期时间(毫秒) */function setLocalStorageWithExpiry(key, value, ttl) { const now = new Date(); // 创建一个包含数据和过期时间的对象 const item = { value: value, expiry: now.getTime() + ttl, }; // 将对象序列化之后存储到localStorage中 localStorage.setItem(key, JSON.stringify(item));}/** * 获取localStorage存储的值 * @param {string} key - 存储的键名 * @returns {*} 存储的值或者当值不存在或过期时返回null */function getLocalStorageWithExpiry(key) { const itemStr = localStorage.getItem(key); // 如果没有找到对应的存储项 if (!itemStr) { return null; } const item = JSON.parse(itemStr); const now = new Date(); // 检查过期时间 if (now.getTime() > item.expiry) { // 如果已过期,删除存储并返回null localStorage.removeItem(key); return null; } // 如果未过期,返回存储的值 return item.value;}// 示例使用// 存储一个名为 'myData' 的数据,过期时间为1小时(3600000毫秒)setLocalStorageWithExpiry('myData', { a: 1, b: 2 }, 3600000);// 获取存储的数据const myData = getLocalStorageWithExpiry('myData');console.log(myData); // 如果还未过期,则会打印出存储的对象 { a: 1, b: 2 }在这个封装的函数中,我们通过 setLocalStorageWithExpiry函数存储数据的时候,会额外添加一个过期时间戳到对象中,并将该对象序列化后保存在localStorage里。当通过 getLocalStorageWithExpiry函数获取数据的时候,我们会先检查当前时间是否已经超过了存储时设置的过期时间戳,如果已经过期,则从localStorage中删除该项,并返回 null;如果未过期,则返回保存的值。
阅读 34·2024年8月5日 04:50

手写 javascript 中 new 的实现过程

当我们在JavaScript中使用new操作符创建一个新对象时,实际上会发生以下几个步骤:创建一个新对象。 使用new操作符时,JavaScript会自动为我们创建一个新的空对象。设置原型链。 新对象内部的[[Prototype]](或者__proto__)属性会被赋值为构造函数的prototype属性,这样新对象就可以访问到构造函数原型上的属性和方法。绑定this并执行构造函数。 构造函数内部的this将会被绑定到新创建的对象上,然后执行构造函数中的代码,这样新对象就可以具有构造函数中定义的属性和方法。返回新对象。 如果构造函数返回的是一个对象,则返回该对象;如果没有返回对象或者返回的不是一个对象,那么将返回步骤1创建的新对象。如果我们要手写一个new的实现,可以定义一个函数来模拟这个过程。以下是一个例子:function myNew(constructor, ...args) { // 步骤1:创建一个空对象,并设置原型链 const obj = Object.create(constructor.prototype); // 步骤2:将构造函数的this绑定到新对象上,并执行构造函数 const result = constructor.apply(obj, args); // 步骤3:根据返回值判断 return result instanceof Object ? result : obj;}// 测试用例function Person(name, age) { this.name = name; this.age = age; this.sayHello = function() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); };}// 使用自定义的myNew来替代new操作符const person = myNew(Person, 'Alice', 30);person.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.以上代码中,myNew函数模拟了new操作符的所有关键步骤,能够模拟出通过new操作符创建对象的效果。
阅读 18·2024年8月5日 04:48

javascript 如何实现高效的字符串前缀匹配

在JavaScript中实现高效的字符串前缀匹配通常可以通过以下几种方式:1. 原生字符串方法使用字符串的startsWith()方法,这是最简单直接的方法,性能也相当好。function isPrefix(str, prefix) { return str.startsWith(prefix);}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false2. 正则表达式利用正则表达式的^锚点来匹配字符串的开头。function isPrefix(str, prefix) { let regex = new RegExp('^' + escapeRegExp(prefix)); return regex.test(str);}// 为了安全性,对特殊字符进行转义,防止注入攻击function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false3. 字符串切片比较通过截取原字符串前N个字符,然后与前缀进行比较。function isPrefix(str, prefix) { return str.slice(0, prefix.length) === prefix;}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false4. 循环比较逐个字符比较,这通常不是最高效的方法,但在某些特定情况下可能是必要的。function isPrefix(str, prefix) { if (str.length < prefix.length) return false; for (let i = 0; i < prefix.length; i++) { if (str[i] !== prefix[i]) { return false; } } return true;}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false5. 使用内置方法 indexOf检查前缀是否在字符串的开头位置。function isPrefix(str, prefix) { return str.indexOf(prefix) === 0;}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false每种方法都有其适用场景,一般而言,如果只需要简单的前缀匹配,推荐使用startsWith()方法,因为它简单且意图明确。如果需要对匹配进行更复杂的控制,可能会选择正则表达式。在处理大量数据或性能至关重要的情况下,可以进行基准测试以确定哪种方法最有效。
阅读 25·2024年8月5日 04:43

javascript 的类型以及如何检测

JavaScript 是一种动态类型语言,这意味着在声明变量时不需要指定数据类型,数据类型会在脚本执行时自动确定。JavaScript 的数据类型主要分为两大类:原始数据类型和对象类型。原始数据类型undefined:表示变量未定义,即声明了变量但未初始化。null:表示一个空值。boolean:布尔类型,有两个值:true 和 false。string:表示文本数据,例如 "Hello, World!"。number:可以是整数或浮点数,例如 42 或 3.14159。bigint:表示大于2^53 - 1的整数。symbol:表示唯一的、不可变的数据值。对象类型Object:JavaScript 中的对象是键值对的集合,几乎所有的 JavaScript 值都是对象类型的,包括数组、函数以及其他内置对象。类型检测的方法在 JavaScript 中,检测变量的类型常用的有几种方法:typeof 运算符:用来检测一个变量的类型,对于原始数据类型非常有效,但对于对象类型和 null,会有一些局限性。let num = 42;console.log(typeof num); // "number"let str = "Hello";console.log(typeof str); // "string"let flag = true;console.log(typeof flag); // "boolean"let bigIntNumber = 1234567890123456789012345678901234567890n;console.log(typeof bigIntNumber); // "bigint"let sym = Symbol('foo');console.log(typeof sym); // "symbol"let und;console.log(typeof und); // "undefined"let obj = { key: 'value' };console.log(typeof obj); // "object"let arr = [1, 2, 3];console.log(typeof arr); // "object", 尽管它是数组let func = function() {};console.log(typeof func); // "function", 函数是对象的一种特殊类型let nul = null;console.log(typeof nul); // "object", 这是一个历史上的错误instanceof 运算符:用来检测一个对象是否是另一个对象的实例。let arr = [1, 2, 3];console.log(arr instanceof Array); // trueconsole.log(arr instanceof Object); // truelet d = new Date();console.log(d instanceof Date); // true// 注意,instanceof 无法检测原始数据类型Array.isArray():用来确定一个值是否是数组。let arr = [1, 2, 3];console.log(Array.isArray(arr)); // true对象的 constructor 属性:可以用来判断对象的构造函数。let arr = [1, 2, 3];console.log(arr.constructor === Array); // truelet obj = {};console.log(obj.constructor === Object); // trueObject.prototype.toString.call():这是一个通用的类型检测方法,可以准确判断各种类型的值。let d = new Date();console.log(Object.prototype.toString.call(d)); // "[object Date]"let num = 42;console.log(Object.prototype.toString.call(num)); // "[object Number]"let str = "Hello";console.log(Object.prototype.toString.call(str)); // "[object String]"注意,当使用类型检测方法时,应当根据具体情况选择最适合的方法,因为每种方法都有其适用场景和限制。
阅读 13·2024年8月5日 04:43

什么是"use strict";?使用它有什么优缺点?​

什么是"use strict"?"use strict"; 是一个JavaScript中的指令,也称作严格模式(strict mode),它用于将整个脚本或单个函数置于一个更加严格的操作环境中。当在代码的开始处使用该指令时,它有助于捕获一些常见的编程错误,同时防止或抛出错误,以及在某些情况下提高编译器的优化水平。由于这些原因,它会改善代码的运行速度和效率。使用它有什么优点?提前捕获错误: 严格模式会在代码执行前就发现一些错误,这些在非严格模式下可能不会被检测到。例如,对不可写的属性赋值,或对只读属性(如undefined,NaN)赋值。避免意外的全局变量: 在严格模式下,如果不使用var、let或const来声明变量,将会抛出错误,这样可以避免全局变量的隐式声明,减少代码中的潜在错误。消除this的混乱: 在严格模式下,如果没有指定上下文对象,函数内的this值将是undefined,这比默认指向全局对象要安全。更安全的eval: 严格模式下,eval函数内部声明的变量不会影响到外部作用域,这使得eval的使用更加安全。提高编译器优化: 代码在执行之前可以进行更多的检查,这为JavaScript引擎的优化打下基础,可能会提高执行速度。使用它有什么缺点?兼容性问题: 在老旧的浏览器或JavaScript环境中,可能不支持严格模式,或者其行为与新版的解释器不一致。代码修改成本: 如果要在已有项目中引入严格模式,可能需要对现有代码进行较大范围的修改,以确保兼容性和正确性。学习曲线: 对于初学者来说,严格模式下的某些限制可能会增加学习难度,因为它们需要更好地理解JavaScript的工作原理。可能隐藏的问题: 在非严格模式写的代码中可能含有在严格模式中会失败的部分,如果不进行彻底的测试,这些隐藏的问题在切换到严格模式后可能会导致运行时错误。示例:以下是一个简单的例子,展示了在使用严格模式时变量必须声明,否则会抛出错误:"use strict";function myFunction() { undeclaredVariable = 123; // 这里会抛出错误,因为变量没有声明}myFunction();如果没有"use strict"; 指令,上面的代码中的undeclaredVariable会被创建为一个全局变量,这可能是一个潜在的问题。使用严格模式,我们可以避免这种情况。
阅读 27·2024年8月5日 04:43