JavaScript
JavaScript有许多内置对象,包括但不限于:
闭包是指一个函数可以访问另一个函数作用域内的变量。当一个函数嵌套在另一个函数中时,内部函数可以访问外部函数的变量,即使外部函数已经返回了。这种情况下,内部函数形成了一个闭包,它保留了外部函数的作用域链并可以继续访问这些变量。闭包常常用于实现函数的封装和私有化,以及在回调和事件处理等场景下的数据共享与传递。
在JavaScript中,作用域、作用域链和执行上下文是密切相关的概念,它们与变量和函数的查找、访问以及生命周期有关。
作用域(Scope):作用域是一个变量或函数的可访问范围。JavaScript中有三种作用域:全局作用域、局部(函数)作用域和块级作用域。全局作用域中声明的变量和函数可以在整个代码中访问,局部作用域中声明的变量和函数只能在特定的函数内部访问,块级作用域在一对花括号内定义,对let
和const
关键字声明的变量有效。变量的生命周期受其作用域的限制。全局作用域中的变量在整个程序执行过程中持续存在,局部作用域中的变量在函数执行结束时销毁,块级作用域在代码块执行结束时,块级作用域中的变量将被销毁。
作用域链(Scope Chain):当代码执行过程中访问一个变量或函数时,JavaScript引擎会沿着作用域链查找该标识符。作用域链是由当前执行上下文的作用域和其所有父级作用域组成的链表。查找过程从当前作用域开始,然后逐级向上查找,直到找到目标标识符或到达全局作用域。如果在全局作用域中仍未找到目标标识符,则返回undefined
。
执行上下文(Execution Context):执行上下文是JavaScript代码执行过程中的环境。每当进入一个新的函数执行或全局代码执行时,都会创建一个新的执行上下文。执行上下文包含了当前执行的代码所需的所有信息,如变量、函数、作用域链等。JavaScript引擎使用执行上下文栈(Execution Context Stack)来管理执行上下文。栈顶的执行上下文为当前执行的代码环境。当一个函数被调用时,一个新的执行上下文被压入栈顶;当函数执行结束时,执行上下文从栈顶弹出,返回到调用者的上下文环境。
总结起来,作用域是变量和函数的可访问范围;作用域链是由当前执行上下文的作用域和其父级作用域组成的链表,用于在代码执行过程中查找变量和函数;执行上下文是代码执行过程中的环境,包含了当前执行的代码所需的所有信息。这三者共同决定了代码执行过程中变量和函数的查找、访问以及生命周期。
可以使用 Object.create(null) 方法创建一个没有原型的对象。这个方法创建一个全新的对象并将其原型设置为 null,因此它没有继承任何属性或方法。例如:
const obj = Object.create(null);
console.log(obj.toString); // undefined
原型链是 JavaScript 中实现继承的一种机制,它通过让一个对象的原型指向另一个对象,从而使得一个对象可以访问另一个对象中定义的属性和方法。当我们试图访问一个对象中不存在的属性或方法时,JavaScript 引擎会沿着原型链一直向上查找,直到找到该属性或方法为止,或者最终抵达 Object.prototype(所有对象的祖先)上停止查找。
var
:声明的变量具有函数作用域。这意味着在函数内部声明的变量只能在该函数内部访问,而在函数外部声明的变量具有全局作用域。let
和const
:声明的变量具有块级作用域。这意味着变量仅在声明它们的代码块(例如:if
语句、for
循环、while
循环等)内部可访问。var
:声明的变量会被提升到所在作用域的顶部。这意味着在声明之前访问变量不会导致引用错误,但变量的值将是undefined
。let
和const
:声明的变量不会被提升。在声明之前访问变量会导致引用错误。var
:允许在同一作用域内多次声明同名变量,后续声明将被忽略。let
和const
:在同一作用域内不允许重复声明同名变量。尝试这样做会导致语法错误。var
和let
:声明的变量可被重新赋值。const
:声明的变量是不可变的,即一旦赋值,无法更改。这对于声明常量或确保某个变量在整个程序执行过程中保持不变的情况非常有用。变量提升(Hoisting)是 JavaScript 的一个核心概念,理解它对于编写和理解代码非常重要。以下是我对变量提升的理解:
undefined
。只有当执行到赋值语句时,它才会被赋予特定的值。var
声明的变量会被提升。用let
和const
声明的变量也有类似的提升行为,但由于它们存在“暂时性死区”(Temporal Dead Zone,TDZ),在声明前对它们的访问会导致错误。JSON.stringify()
是一个将JavaScript对象转换为JSON字符串的方法。尽管它在许多情况下非常有用,但它确实存在一些限制和缺点:
JSON.stringify()
无法处理具有循环引用的对象。如果一个对象的属性直接或间接引用了自身,JSON.stringify()
将抛出一个错误,表示存在循环引用。undefined
、函数和Symbol忽略:JSON.stringify()
不会序列化对象中的undefined
、函数和Symbol类型的属性。这些属性将被忽略,不会出现在生成的JSON字符串中,单独转换则会返回undefined
。JSON.parse()
)后,原始对象的原型链信息将不复存在。JSON.stringify()
序列化日期对象时,日期对象会被转换为它们的ISO字符串表示形式。在反序列化时,这些日期将被视为普通字符串,而不是日期对象。JSON.stringify()
会直接返回其对应的JSON表示,而不会将其包装在对象或数组中。for…in循环用于遍历对象的可枚举属性,返回的是属性名称;for…of循环用于遍历可迭代对象(如数组、字符串、Map、Set等),返回的是元素值。
__proto__
指向构造函数的prototype
this
并调用类数组(Array-like)和数组(Array)都是用于存储多个值的数据结构,但它们之间存在一些关键区别:
Array.prototype
,具有一系列数组方法(如push()
、pop()
、map()
等)。类数组是普通的对象,其属性名为索引(如0
、1
、2
等),具有一个length
属性,但不具备数组的方法。Array.prototype
,因此具有数组的所有方法。类数组的原型通常为Object.prototype
,并不包含数组的方法。要将DOM的类数组(例如,通过document.getElementsByClassName()
或document.querySelectorAll()
获取的元素集合)转换为数组,可以使用以下方法之一:
使用Array.from()
方法:
let nodeList = document.querySelectorAll(‘div’);
let array = Array.from(nodeList);
Array.from()
方法会创建一个新数组,并将类数组的元素逐个复制到新数组中。
使用扩展运算符(Spread Operator):
let nodeList = document.querySelectorAll(‘div’);
let array = […nodeList];
扩展运算符...
可以将类数组直接转换为数组。
使用Array.prototype.slice.call()
:
let nodeList = document.querySelectorAll(‘div’);
let array = Array.prototype.slice.call(nodeList);
Array.prototype.slice.call()
方法会将类数组作为上下文,并创建一个新数组,将类数组的元素逐个复制到新数组中。
这些方法可以将类数组转换为数组,这样就可以在转换后的数组上使用数组的方法了。注意,这些方法不仅适用于DOM类数组,还适用于其他类数组对象。
offsetWidth/offsetHeight是元素的可见宽度/高度加上padding、border和滚动条(如果存在)的宽度/高度。
clientWidth/clientHeight是元素的可见宽度/高度,不包括padding和滚动条。
scrollWidth/scrollHeight是元素内容的完整宽度/高度,包括溢出部分。如果元素没有溢出,则scrollWidth/scrollHeight等于clientWidth/clientHeight。如果有溢出,则scrollWidth/scrollHeight大于clientWidth/clientHeight。
mouseover和mouseout是HTML DOM事件,它们会在鼠标移入或移出元素时触发。它们也会在鼠标指针进入或离开子元素时触发。这也就是说,如果在父元素上有mouseover事件,并且鼠标指针进入子元素,则该元素上仍然会触发mouseover事件。mouseout同理。
mouseenter和mouseleave事件也是在鼠标进入或离开元素时触发。与mouseover和mouseout不同的是,mouseenter和mouseleave事件不会传播到子元素。因此,如果鼠标指针进入或离开元素的子元素,则不会触发mouseenter和mouseleave事件。
event.stopPropagation()可以阻止事件冒泡到父元素,但不阻止其他事件处理程序的执行。而event.stopImmediatePropagation()可以立即阻止事件冒泡并取消同一元素上其他事件处理程序的执行。
事件循环(Event Loop)是 JavaScript 运行时环境中的一个核心概念,它负责协调异步操作和同步代码的执行。JavaScript 是单线程的,这意味着它一次只能执行一个任务。事件循环使 JavaScript 能够在执行同步代码的同时,处理异步操作(如定时器、用户交互和网络请求)的回调。
事件循环的工作原理大致如下:
标签或 Node.js 文件的代码)。setTimeout
、setInterval
、Promise
、fetch
等),它们的回调函数会被放入相应的任务队列中(微任务队列或宏任务队列)。事件循环的目标是在处理同步代码和异步回调之间保持平衡,确保 JavaScript 代码的执行效率和响应能力。通过这种方式,事件循环允许 JavaScript 在单线程环境中有效地处理并发操作。
ESM(ECMAScript Modules)和 CommonJS 是 JavaScript 中两种不同的模块系统。它们都允许将代码拆分成可重用的模块,并在需要时导入这些模块。尽管它们都实现了相似的功能,但它们之间存在一些关键差异:
import
和 export
关键字require
和 module.exports
关键字global
对象访问全局作用域。总结一下,ESM 和 CommonJS 的主要区别在于它们的语法、加载机制、作用域、循环依赖处理、兼容性和使用场景以及实时绑定与值拷贝。尽管它们在某些方面有所不同,它们都是为了解决 JavaScript 模块化编程的问题。
在 JavaScript 中,栈内存(Stack Memory)和堆内存(Heap Memory)扮演着不同的角色,它们分别负责存储不同类型的数据。以下是它们在 JavaScript 中的简要说明:
number
、string
、boolean
、null
和 undefined
。这些类型的值通常较小且固定大小。object
)、数组(array
)和函数(function
)。这些类型的值通常较大,大小不固定。总结一下,在 JavaScript 中,栈内存用于存储基本类型的值、函数调用的执行上下文和局部变量,堆内存用于存储引用类型的值。理解栈内存和堆内存的差异有助于编写高效且内存友好的 JavaScript 程序。
箭头函数(Arrow Functions)与普通函数(常被称为函数声明或函数表达式)在 JavaScript 中有一些重要的区别。这些区别包括语法、this
关键字的绑定、arguments 对象的使用、构造函数行为以及原型链。以下是箭头函数和普通函数之间的主要区别:
this
关键字绑定:this
,它从包围它的普通函数或全局作用域继承 this
。这使得在事件处理器或回调函数中使用箭头函数非常方便,因为它们自动捕获外部的 this
。this
,它的值在函数调用时确定。根据函数调用的方式(如通过对象方法调用、直接调用、构造函数调用等),this
的值可能会有所不同。arguments
对象。它们可以访问包围它们的普通函数的 arguments
对象。arguments
对象,这是一个类数组对象,包含了传递给函数的参数。new
关键字调用。它们也没有 prototype
属性。new
关键字创建新的对象实例。prototype
属性,它们不能作为其他对象的原型。prototype
属性,可以作为其他对象的原型。yield
关键字。yield
关键字变成生成器函数。箭头函数的 this
是根据其被声明的位置来确定的,而不是它被调用的位置。这个特性称为"词法作用域"或者"静态作用域"。箭头函数不会创建自己的 this
,它会从自己的作用域链上一层继承 this
。
isNaN函数用于检查一个值是否是NaN,它会将传入的参数先转换为数字类型再进行判断。如果传入的参数无法转换为数字类型,则会返回true。
而Number.isNaN用于检查一个值是否为NaN,但它不会将参数转换为数字类型,只有在参数本身就是NaN时才返回true。否则,返回false。
在 JavaScript 中,this
是一个特殊的关键字,它在函数调用时动态地引用了一个对象。this
的值取决于函数的调用方式,不同的调用方式会导致 this
指向不同的对象。以下是一些关于 this
的不同用法和场景:
全局上下文:当在全局作用域中使用 this
时,它指向全局对象。在浏览器环境中,全局对象是 window
;在 Node.js 环境中,全局对象是 global
。
函数调用:当在函数内部使用 this
且函数作为普通函数调用时(非对象方法调用),this
通常指向全局对象。但在严格模式下(使用 "use strict"
),this
会被设置为 undefined
。
对象方法调用:当在对象的方法内部使用 this
时,this
指向调用该方法的对象。这也适用于原型链中的方法。
构造函数调用:当在构造函数内部使用 this
且使用 new
关键字调用构造函数时,this
指向新创建的对象实例。
显式绑定:使用 call
、apply
或 bind
方法调用函数时,可以显式地将 this
绑定到一个指定的对象。
箭头函数:箭头函数没有自己的 this
,它从包围它的普通函数或全局作用域继承 this
。这使得在事件处理器或回调函数中使用箭头函数非常方便,因为它们自动捕获外部的 this
。
总之,this
是 JavaScript 中一个动态上下文的关键字,它的值取决于函数调用的方式。
在JavaScript中,严格模式(strict mode)和非严格模式(sloppy mode)主要有以下几个区别:
let
、const
或var
关键字)。否则,将会抛出一个引用错误(ReferenceError)。在非严格模式下,如果没有声明变量,JavaScript会自动将其声明为全局变量,这可能会导致意外的全局污染。this
值为undefined
。在非严格模式下,全局作用域中的this
值为全局对象(浏览器环境中为window
对象,Node.js环境中为global
对象)。此外,在严格模式下,不允许使用call
、apply
或bind
将this
值设置为null
或undefined
。implements
、interface
、let
、package
、private
、protected
、public
、static
和yield
)用作变量名或函数名。0123
)。非严格模式下,八进制字面量是允许的。delete
操作符删除变量、函数和函数参数会引发语法错误(SyntaxError)。在非严格模式下,这样的操作是允许的,但实际上不会删除这些对象。要启用严格模式,可以在脚本或函数开头添加"use strict";
指令。这将对整个脚本或函数体中的代码启用严格模式。推荐使用严格模式编写代码,因为它可以帮助发现潜在的错误并避免一些不良的编程实践。
Promise是一种在JavaScript中用于处理异步操作的编程模式。它表示一个尚未完成但预计在未来某个时刻完成的操作的结果。Promise允许我们以更简洁、易读的方式处理异步操作,避免了传统的回调地狱(callback hell)问题。
Promise有三种状态:
Promise具有以下特点:
缺点:
Promise基本用法包括:
new Promise(executor)
创建一个Promise对象,其中executor是一个执行器函数,接受两个参数:resolve和reject。成功时调用resolve函数并传递结果,失败时调用reject函数并传递原因。.then()
方法处理fulfilled状态,接受一个回调函数作为参数,当Promise状态变为fulfilled时调用。.catch()
方法处理rejected状态,接受一个回调函数作为参数,当Promise状态变为rejected时调用。通过使用Promise,我们可以更有效地处理异步操作,降低代码复杂性,提高可维护性。在现代JavaScript开发中,Promise已成为处理异步操作的重要基石。
在JavaScript(以及许多其他编程语言)中,0.1 + 0.2 不等于 0.3 的原因是浮点数精度问题。JavaScript使用IEEE 754标准中规定的双精度浮点数(double-precision floating point)来表示数字。这种表示方法在大多数情况下都很有效,但有时会导致精度损失。
双精度浮点数只有有限的位数(64位)来表示数字,其中1位表示符号位,11位表示指数,以及52位表示尾数。当尝试表示某些数字(特别是十进制小数)时,它们的二进制表示可能是无限循环的,因此需要截断以适应有限的位数。这可能导致浮点数的近似值与实际值之间存在微小差异。
在本例中,0.1和0.2的二进制表示都是无限循环的,需要截断。当它们被截断并以双精度浮点数存储时,这两个数字的实际值与理论值略有不同。因此,当执行0.1 + 0.2时,结果也会有微小误差,与0.3的理论值不完全相等。
为了解决这个问题,可以将结果四舍五入到所需的精度。例如,如果要比较两个数字是否相等,可以将它们四舍五入到一个合理的精度,然后再进行比较:
function areNumbersAlmostEqual(num1, num2, epsilon = 1e-10) {
return Math.abs(num1 - num2) < epsilon;
}
console.log(areNumbersAlmostEqual(0.1 + 0.2, 0.3)); // 输出 true
在这个示例中,我们使用一个称为"epsilon"的小数值来表示可接受的误差范围。我们计算两个数字之差的绝对值,如果它小于epsilon,我们认为这两个数字几乎相等。在实践中,需要根据具体问题选择合适的epsilon值。
Map 和 WeakMap 在 JavaScript 中都提供了键值对的存储,但它们的工作方式和底层实现有一些重要的区别。
键的引用:在 Map 中,键的引用是强引用,也就是说只要 Map 存在,那么它的键值对就会保留在内存中,不会被垃圾收集器回收。而在 WeakMap 中,键的引用是弱引用,也就是说如果没有其他地方引用该键,那么该键就会被垃圾收集器回收,不论该 WeakMap 是否还存在。
键的类型:在 Map 中,键可以是任何类型,包括原始类型(比如字符串、数字、布尔值)和对象类型。而在 WeakMap 中,键必须是对象。
迭代器和清除方法:Map 具有诸如 size
、clear
、keys
、values
和 entries
等方法,允许开发者获取大小,清除所有键值对,或者迭代所有的键或值。然而,由于 WeakMap 的键是弱引用,为了防止在垃圾回收过程中可能引发的并发问题,WeakMap 没有这些方法。
底层实现:JavaScript 本身是高级语言,其具体实现取决于底层的 JavaScript 引擎,如 V8 或 SpiderMonkey。在一般情况下,Map 可以使用简单的哈希表来实现。对于 WeakMap,由于其键是弱引用,因此在内存管理方面需要更加复杂的处理。这些处理通常在引擎级别完成,而不是在 JavaScript 代码级别。
以上这些区别使得 Map 和 WeakMap 有各自适用的情况。比如,当你需要存储的键值对在某个时间点之后不再需要时,使用 WeakMap 可以防止内存泄漏。而当你需要完全控制何时删除键值对时,使用 Map 更为合适。