14、new操作符具体做了什么
new
操作符用于创建一个用户定义的对象类型的实例。它的具体步骤如下:
- 创建一个新的空对象。
- 将新对象的原型指向构造函数的
prototype
属性,以便实例能够继承构造函数的属性和方法。 - 将构造函数内部的
this
绑定到新对象上,使构造函数内部的操作能在新对象上执行。 - 执行构造函数内部的代码,对新对象进行初始化操作,设置实例特有的属性和方法。
- 如果构造函数没有显式地返回一个对象,则返回新创建的对象;否则,返回显式返回的对象。
以下是一个示例来说明 new
操作符的使用:
// 定义一个构造函数 function Person(name, age) { this.name = name; this.age = age; } // 使用 new 操作符创建实例 const person1 = new Person('Alice', 25); console.log(person1); // 输出: Person { name: 'Alice', age: 25 }
在这个示例中,我们定义了一个构造函数 Person
,它接受两个参数 name
和 age
,并将它们分别赋值给新创建的实例的 name
和 age
属性。通过使用 new
操作符,我们创建了一个名为 person1
的 Person
类型的实例。
总结起来,new
操作符的主要作用是创建一个实例对象,并对其进行初始化。它涉及到创建一个新对象、绑定原型、执行构造函数内部代码等步骤,最后返回这个新创建的实例。
15、说说你对闭包的理解
闭包是指一个函数能够访问并操作其词法作用域外部的变量的能力。简单来说,闭包可以通过记住和访问定义时所处的环境,即使在其定义所在的作用域外部被调用,仍然可以访问这些环境中的变量。
闭包的特点如下:
- 函数嵌套:闭包一般由一个函数内部定义另一个函数形成。
- 内部函数访问外部变量:内部函数可以访问外部函数的变量和参数,即使外部函数已经执行完毕。
- 外部变量的保存:由于闭包会持有对外部变量的引用,因此这些变量不会被垃圾回收机制回收,在闭包存在的情况下,外部变量的值会一直保存下来。
闭包的应用场景包括但不限于:
- 封装私有变量和方法:通过闭包,可以创建具有私有变量和方法的模块化代码。
- 实现函数柯里化:通过闭包可以生成柯里化函数,减少重复的代码。
- 延迟执行:通过闭包可以实现延迟执行的效果,例如在定时器和事件监听器中使用闭包。
以下是一个简单的示例,展示了闭包的使用:
function outer() { var outerVar = 'Hello'; function inner() { var innerVar = 'World'; console.log(outerVar + ' ' + innerVar); } return inner; } var closure = outer(); closure(); // 输出: Hello World
在这个示例中,outer
函数内部定义了 inner
函数,并将其作为结果返回。当调用 outer
函数时,它会返回一个对 inner
函数的引用,形成闭包,并且 outerVar
变量在闭包中被保留。之后,通过调用闭包 closure
,我们可以访问并输出 outerVar
和 innerVar
变量的值。
需要注意的是,过度滥用闭包可能会导致内存泄漏问题,因为闭包会持有外部变量的引用,导致这些变量无法被垃圾回收。因此,在使用闭包时需要谨慎考虑内存管理的问题。
16、说说你对原型链的理解
原型链是 JavaScript 中一种对象之间通过原型(prototype)关联的机制。对象可以通过继承其它对象的属性和方法,这些被继承的属性和方法存储在对象的原型中。每个对象都有一个原型,并且可以通过该原型链接到其父级对象的原型,形成一个原型链。
在原型链中,如果我们要访问一个对象的属性或方法,JavaScript 引擎首先会在对象本身查找,如果没有找到,则会继续在该对象的原型中查找。如果还没有找到,则继续在原型的原型中查找,直到找到目标属性或方法,或者到达原型链的末端(Object.prototype)。
以下是一个简化的原型链示意图:
Object -> Object.prototype -> null \ -> Function -> Function.prototype -> Object.prototype -> null \ -> CustomObject -> CustomObject.prototype -> Object.prototype -> null
在这个示意图中,Object
是所有对象的基础,它的原型是 Object.prototype
。Function
是所有函数对象的构造函数,它的原型是 Function.prototype
,同时也继承了 Object.prototype
。CustomObject
是由自定义构造函数创建的对象,它的原型是 CustomObject.prototype
,同时也继承了 Object.prototype
。最终的原型链指向了 Object.prototype
,这是原型链的末端。
当我们访问一个对象的属性或方法时,JavaScript 引擎会按照原型链的顺序依次查找,直到找到目标属性或方法,或者到达原型链的末端。这使得对象可以继承和共享属性和方法,提供了一种灵活而高效的方式来组织和重用代码。
以下是一个简单的示例,展示了原型链的使用:
function Person(name) { this.name = name; } Person.prototype.greet = function() { console.log('Hello, my name is ' + this.name); }; var person = new Person('Alice'); person.greet(); // 输出: Hello, my name is Alice
在这个示例中,Person
是一个构造函数,通过 new
关键字创建了一个 person
对象。Person.prototype
是 person
对象的原型,通过设置 greet
方法,所有由 Person
构造函数创建的对象都会继承并共享这个方法。因此,我们可以通过 person.greet()
调用 greet
方法,并在控制台输出打招呼的信息。
总结起来,原型链是 JavaScript 中实现继承和共享属性和方法的机制。每个对象都有一个原型,通过原型链的关联,可以在对象之间进行属性和方法的继承和共享。
17、说说call、apply、bind区别?
call
、apply
和 bind
都是 JavaScript 中用于改变函数执行上下文(即 this
的指向)的方法。它们之间的区别主要体现在参数的传递方式和函数执行的时间。
call
方法:
- 语法:
function.call(thisArg, arg1, arg2, ...)
- 参数:
thisArg
是函数执行时的上下文对象,后续参数arg1, arg2, ...
是被调用函数的参数列表。 - 作用:立即调用函数,并将指定的
this
值和参数传递给函数。 - 示例:
function greet(name) { console.log('Hello, ' + name); } greet.call(null, 'Alice'); // 输出: Hello, Alice
apply
方法:
- 语法:
function.apply(thisArg, [argsArray])
- 参数:
thisArg
是函数执行时的上下文对象,argsArray
是一个数组或类数组对象,包含被调用函数的参数。 - 作用:立即调用函数,并将指定的
this
值和参数数组传递给函数。 - 示例:
function greet(name) { console.log('Hello, ' + name); } greet.apply(null, ['Alice']); // 输出: Hello, Alice
bind
方法:
- 语法:
function.bind(thisArg, arg1, arg2, ...)
- 参数:
thisArg
是函数执行时的上下文对象,后续参数arg1, arg2, ...
是被调用函数的预设参数。 - 作用:创建一个新的函数,该函数会把指定的
this
值和参数传递给原始函数,并且不会立即执行原始函数,而是返回一个绑定了上下文和参数的新函数。 - 示例:
function greet(name) { console.log('Hello, ' + name); } var greetAlice = greet.bind(null, 'Alice'); greetAlice(); // 输出: Hello, Alice
总结起来,call
和 apply
会立即调用原始函数,并传递指定的上下文对象和参数,而 bind
则会创建一个新的函数,绑定了上下文对象和参数,并且不会立即执行原始函数。需要根据具体情况选择使用不同的方法。
18、深拷贝和浅拷贝
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在编程中用于复制对象或数组的概念。它们之间的区别在于复制的方式和复制后的结果。
- 浅拷贝:
- 浅拷贝创建一个新对象或数组,并将原始对象或数组的引用复制给新对象或数组。这意味着当修改其中一个对象或数组的值时,会影响到另一个对象或数组。
- 浅拷贝适用于简单的数据结构,如基本类型、普通对象或一维数组。
- 示例:
// 对象的浅拷贝 let obj = { a: 1, b: { c: 2 } }; let shallowCopy = Object.assign({}, obj); obj.b.c = 3; console.log(shallowCopy.b.c); // 输出: 3 // 数组的浅拷贝 let array = [1, [2, 3]]; let shallowCopy = array.slice(); array[1][0] = 4; console.log(shallowCopy[1][0]); // 输出: 4
- 深拷贝:
- 深拷贝创建一个全新的对象或数组,并递归地将原始对象或数组的所有值复制给新对象或数组。这样,新旧对象或数组是完全独立的,修改其中一个不会影响到另一个。
- 深拷贝适用于复杂的数据结构,如嵌套对象、多维数组等。
- 示例:
// 对象的深拷贝 let obj = { a: 1, b: { c: 2 } }; let deepCopy = JSON.parse(JSON.stringify(obj)); obj.b.c = 3; console.log(deepCopy.b.c); // 输出: 2 // 数组的深拷贝 let array = [1, [2, 3]]; let deepCopy = JSON.parse(JSON.stringify(array)); array[1][0] = 4; console.log(deepCopy[1][0]); // 输出: 2
需要注意的是,深拷贝有一些限制:
- 无法拷贝函数、正则表达式、
undefined
等特殊对象。 - 循环引用会导致无限递归,需要额外处理。
深拷贝可以使用递归函数、自定义实现或第三方库(如 lodash.cloneDeep()
)来完成,选择合适的方式取决于具体情况和需求。
19、localStorage、sessionStorage、cookie的区别
localStorage、sessionStorage和cookie都是在浏览器端存储数据的方式,它们之间有以下区别:
- 存储容量:
- localStorage和sessionStorage:每个域名下的localStorage和sessionStorage默认有5MB的存储容量。两者的容量是共享的,即同一域名下的所有页面都可以访问和修改存储的数据。
- cookie:每个域名下的cookie容量一般为4KB,每个域名下最多可以设置20个cookie。
- 生命周期:
- localStorage:存储的数据没有过期时间,除非主动删除或清除浏览器缓存,否则会一直存在。
- sessionStorage:存储的数据在当前会话结束时(关闭浏览器标签页或窗口)被清除,重新打开标签页或窗口会生成一个新的会话,数据也会消失。
- cookie:可以设置一个过期时间,当超过过期时间后,cookie会自动被删除。
- 数据交互:
- localStorage和sessionStorage:只能存储字符串类型的数据。可以使用JSON.stringify()和JSON.parse()方法对对象进行序列化和反序列化。
- cookie:可以存储字符串类型的数据,但也可以在设置和读取时使用encodeURIComponent()和decodeURIComponent()等方法操作其他类型的数据。
- 与服务器的通信:
- localStorage和sessionStorage:存储在浏览器端,与服务器通信时不会自动携带,需要手动添加到请求头或请求体中。
- cookie:在与服务器通信时,会自动携带在请求头中。
- 安全性:
- localStorage和sessionStorage:存储在浏览器端,相对较安全。但如果网站存在跨站脚本攻击(XSS),恶意脚本可能能够访问和修改存储的数据。
- cookie:存在一定的安全风险,因为它们可以被窃取或篡改。可以通过设置cookie的
HttpOnly
属性来增加一定的安全性,防止客户端脚本访问cookie值。
根据具体的需求和场景选择合适的存储方式。如果需要长期存储大量数据,可以选择localStorage;如果需要临时存储会话相关的数据,可以选择sessionStorage;如果需要与服务器交互或存储少量数据,可以选择cookie。
20、事件循环event loop的理解?
事件循环是指在异步编程中用来处理事件的一种机制。它主要由事件轮询、任务队列和事件处理器组成。在事件循环中,程序会不断地从事件轮询中获取事件,如果有事件,就将其加入到任务队列中。任务队列是一个先进先出的数据结构,在任务队列中的任务会被按照顺序依次执行。一旦任务队列中的任务全部执行完毕,程序会继续从事件轮询中获取事件。
事件循环的本质是一个事件驱动模型,它可以处理各种类型的异步任务,包括I/O操作、计时器等等。通过事件循环,程序可以在不阻塞主线程的情况下处理大量的任务,提高程序的并发性能和响应速度。
在JavaScript中,事件循环是由浏览器或Node.js平台提供的,开发者只需要编写能够响应事件的异步函数就可以了。例如,在浏览器中,可以通过addEventListener函数来监听事件,然后在回调函数中执行异步任务。在Node.js中,可以使用各种I/O库来处理异步事件。
21、前端跨域解决方案?
前端跨域解决方案主要包括以下几种:
- JSONP:通过动态创建script标签,利用src属性跨域获取数据。
- CORS:需要服务器设置响应头Access-Control-Allow-Origin,允许指定的域名进行跨域访问。
- 代理服务器:在项目后台部署一个代理服务器,再通过该服务器转发请求,避免浏览器的跨域限制。
- WebSocket:基于HTTP协议,可以实现客户端和服务器之间的双向通信,不受跨域限制。
- postMessage:H5提供的API,可以实现不同窗口和文档之间的安全数据传递,也不受跨域限制。
22、数组常用方法及作用?
- push(): 在数组末尾添加一个或多个元素,并返回新的数组的长度。
- pop(): 删除数组末尾的最后一个元素,并返回被删除的元素。
- shift(): 删除数组中的第一个元素,并返回被删除的元素。同时将数组中的所有元素向左移一位。
- unshift(): 在数组的开始位置添加一个或多个元素,并返回新的数组的长度。
- splice(): 在数组中添加或删除元素。它可以接受多个参数:
a) 第一个参数:起始位置。
b) 第二个参数:删除元素的数量。
c) 第三个以及之后的参数:要添加的元素。 - slice(): 从数组中返回指定部分的新数组,而不修改原数组。它可以接受两个参数,分别为起始位置和结束位置。
- concat(): 合并两个或多个数组,并返回新的数组。
- reverse(): 将数组中的元素顺序翻转,原数组会被直接修改。
- join(): 将数组中的所有元素转换为一个字符串,并返回该字符串。它可以接受一个可选参数,用于指定元素之间的分隔符。
- toString(): 将数组转换为字符串,并返回该字符串。
- includes(): 判断数组是否包含给定的值,并返回一个布尔值。
- indexOf(): 返回数组中指定元素的位置,如果不存在则返回-1。
- forEach(): 对数组中的每一个元素执行指定的操作。
- map(): 对数组中的每一个元素执行指定的操作,并返回新的数组。
- filter(): 对数组中的每一个元素执行指定的条件,符合条件的元素将被添加到新的数组中,并返回该数组。
- reduce(): 对数组中的所有元素进行计算,返回一个新的值。它可以接受两个参数:
a) 回调函数:指定对每个元素进行的计算操作。
b) 初始值:在计算操作开始前使用的值。
23、for…in 循环和for…of循环的区别
for…in循环是用于遍历对象属性的循环,而for…of循环是用于遍历可迭代对象(例如数组、Map、Set等)中的元素的循环。
for…in循环返回的是属性名,而for…of循环返回的是元素值。
for…in循环可以遍历到原型链上的所有可枚举属性,而for…of循环不能。
for…in循环是一个比较旧的语法,for…of循环是ES6新增的语法。
总的来说,for…in循环适合遍历对象的属性,而for…of循环适合遍历数组、Map、Set等可迭代对象的元素。
24、js数据类型判断都有哪几种方式?它们的区别是什么?
JavaScript数据类型判断的几种方式包括:
- typeof操作符
- instanceof操作符
- Object.prototype.toString方法
- constructor属性
这些方式的区别在于,typeof操作符可以判断出基本数据类型(如数值、字符串、布尔值、undefined),以及函数类型;instanceof操作符可以判断出对象的具体类型,但不能判断基本数据类型;Object.prototype.toString方法可以返回值的详细类型信息;constructor属性可以返回创建该实例的构造函数,但可能被修改导致不准确。因此,在实际应用中,需要根据具体情况选择合适的方式进行数据类型判断。
25、说说你对Object.defineProperty()的理解?
Object.defineProperty()是一个JavaScript API,它允许开发者定义一个对象的属性,该API可以在创建对象时或在后期定义对象属性时使用。通过此API,开发人员可以修改属性的属性描述符,例如,控制属性的可读性、可写性、枚举性和可配置性。这个方法可以被用来定义新属性或者修改现有的属性。
Object.defineProperty()可以用于实现一个双向绑定,监听对象属性的变化时,首先要通过该API设置对象属性的getter和setter方法,然后在getter方法中添加依赖收集器,将观察者加入到该属性依赖收集器进行依赖收集,当该属性发生了变化时就进行触发,然后在setter方法中将该属性对应的观察者进行更新。
26、说说javascript内存泄漏的几种情况?
- 垃圾回收机制不足
JavaScript是通过垃圾回收器来管理内存的,当变量不再引用一个对象时,垃圾回收机制会自动释放这个对象所占用的内存。但如果程序中存在循环引用,垃圾回收机制可能无法正常工作,导致内存泄漏。 - 闭包
闭包是指能够访问父级作用域中变量的内部函数。如果一个函数被定义在另一个函数内部,并返回了对它的引用,这就会形成闭包。如果这个闭包中存储了一些全局变量的引用,那么这些变量就永远无法被释放,导致内存泄漏。 - DOM操作
在JavaScript中,通过操作DOM来动态创建、删除和更新HTML元素。但是,如果不妥善处理DOM,例如忘记删除已经不需要的元素,那么这些元素就会一直占据内存,导致内存泄漏。 - 定时器
通过setTimeout和setInterval可以实现定时执行一段代码,但如果不及时清除定时器,当定时器不再需要时就会一直占据内存,导致内存泄漏。 - 内存泄露的标识
在一些框架或者库中可能会出现内存泄漏的情况,例如一些不当的缓存机制、事件监听器没有被正确的移除等等,如果不及时处理,这些标识就会一直存在,导致内存泄漏。
27、如何通过原生js实现一个节流函数和防抖函数,写出核心代码,不是简单的思路?
当使用原生JavaScript实现节流函数和防抖函数时,以下是它们的核心代码:
- 节流函数:
function throttle(func, delay) { let timerId; return function() { if (!timerId) { timerId = setTimeout(() => { func.apply(this, arguments); timerId = null; }, delay); } }; }
使用示例:
function handleScroll() { console.log("Throttled scroll event"); } window.addEventListener("scroll", throttle(handleScroll, 200));
- 防抖函数:
function debounce(func, delay) { let timerId; return function() { clearTimeout(timerId); timerId = setTimeout(() => { func.apply(this, arguments); }, delay); }; }
使用示例:
function handleInput() { console.log("Debounced input event"); } const inputElement = document.getElementById("myInput"); inputElement.addEventListener("input", debounce(handleInput, 300));
在上述代码中,throttle
函数接受一个函数参数 func
和一个延迟时间参数 delay
。它返回一个新的函数,该函数在指定的延迟时间内只会执行一次 func
函数。当触发事件时,首先检查timerId
是否存在,若不存在则通过 setTimeout
启动计时器,在延迟时间过后执行 func
函数并将 timerId
设置为 null
。
而 debounce
函数也接受一个函数参数 func
和一个延迟时间参数 delay
。它返回一个新的函数,该函数在触发事件后的延迟时间内不会立即执行 func
函数,而是会清除之前的计时器,并重新设置一个新的计时器。如果在延迟时间内再次触发事件,将清除上一个计时器,并重新开始计时。只有在延迟时间后没有再次触发事件时,才会执行 func
函数。
这些节流函数和防抖函数可以帮助我们控制事件的触发频率,优化性能并提升用户体验。
28、为什么for循环比forEach性能高?
一般情况下,for
循环比forEach
性能高的原因是因为它更为底层,具有更高的执行效率和更少的额外开销。
以下是一些可能导致for
循环性能优于forEach
的原因:
- 遍历方式:
for
循环使用简单的索引方式进行遍历,而forEach
方法需要使用回调函数来迭代数组中的每个元素。回调函数可能会导致额外的函数调用开销。 - 遍历控制:
for
循环可以通过控制条件来实现灵活的遍历控制,例如使用break
和continue
语句。而forEach
方法是一个固定的迭代过程,不支持直接的控制流程。 - 作用域链:
forEach
方法的回调函数会在每次迭代时创建一个新的作用域。这可能会导致一定的内存和性能开销。而for
循环只需要一个作用域。 - 数组访问:在一些 JavaScript 引擎中,
forEach
方法在迭代数组时使用了一些额外的性能保护措施,例如对数组长度进行缓存,以确保在迭代过程中数组不会被修改。这可能会导致一些额外的性能开销。
需要注意的是,性能差异可能在不同的环境和具体实现中有所不同,因此在实际使用时,应根据具体情况进行评估和测试。在大多数情况下,两种方式的性能差距是微小的,对于大多数应用而言,并不会对整体性能产生显著影响。因此,在代码可读性和可维护性之间做出适当的权衡也是很重要的。
29、Js的数据类型和内存存储区别
JavaScript 的数据类型包括基本数据类型(primitive types)和引用数据类型(reference types)。
基本数据类型:
- 数字(number):表示数字,包括整数和浮点数。
- 字符串(string):表示一串字符。
- 布尔值(boolean):表示真或假。
- null:表示空值。
- undefined:表示未定义的值。
- Symbol:表示唯一和不可变的值,用于创建对象的属性名。
引用数据类型: - 对象(object):表示一个无序属性的集合,可以包含基本数据类型或其他对象。
- 数组(array):是一种特殊的对象,用于存储一组有序的数据。
- 函数(function):是一种特殊的对象,可以被调用执行。
基本数据类型的值被直接存储在变量访问的地方,而引用数据类型的值则是存储在堆内存中的对象,变量保存的只是该对象的地址。这意味着对于基本数据类型,变量的复制会创建一个新的值,而对于引用数据类型,则传递的是地址,所以任何改变都会影响到原始对象的值。当不再需要一个对象时,Javascript 的垃圾收集器会自动回收它的堆内存。
30、说说什么是闭包,有哪些应用场景?
闭包(Closure)是指在一个函数内部创建另一个函数,并返回这个函数的同时,将它所在的上下文环境一起返回。换句话说,闭包可以访问定义自己的函数体外部的变量和函数,即使在外部函数已经执行结束、上下文环境已经销毁的情况下也仍然可以访问。
简单来说,闭包就是可以访问外层函数作用域中的变量和函数的函数,这种特性使得 JavaScript 可以实现很多有用的功能和设计模式。以下是一些常见的应用场景:
- 封装私有变量和方法:在一个函数内部定义局部变量和方法,然后再返回一个新的函数,新函数通过闭包访问并返回这些属性和方法,从而实现对外不可访问的“私有”属性和方法。
- 模块化开发:使用闭包可以实现模块化的代码组织方式,将相关的变量和函数封装在一个函数作用域内,并通过返回值暴露出需要对外公开的接口,以实现代码的解耦和复用。
- 延迟执行:通过使用闭包可以创建一个作用域独立、拥有持久性状态的函数,该函数可以延迟执行或按需执行,例如事件处理函数、定时器等。
- 科里化:利用闭包可以将一个多参数的函数转化为一系列单参数函数的嵌套调用,从而实现更加灵活和易于组合的调用方式。
总之,闭包是 JavaScript 中非常强大和有用的特性,掌握闭包具有重要的意义,可以提高代码的性能、可读性和可维护性。
31、说说你对原型和原型链的理解?场景
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]]
(也称为“隐式原型”),它指向该对象的原型。原型是一个对象,它包含可以被该对象实例所继承的属性和方法。而原型链就是由一连串的原型对象组成的链式结构,用于查找和继承对象的属性和方法。
具体来说,当我们使用“点”运算符访问一个对象的属性时,如果该对象本身没有定义该属性,那么 JavaScript 就会沿着原型链依次查找,直到找到包含该属性的原型对象或者找到顶层的 Object.prototype 为止。如果最终都没有找到,则返回 undefined。
以下是一些常见的原型和原型链的应用场景:
- 原型继承:JavaScript 中的继承是基于原型链实现的。我们可以通过将一个对象的原型设置为另一个对象的实例来实现继承,并以此实现代码的复用和解耦。
- 对象操作:由于 JavaScript 中的所有对象都具有原型,因此我们可以通过修改对象原型中的属性和方法,来扩展和增强其功能。例如,可以为数组原型添加一个新的方法,以实现数组的高级操作。
- 访问器属性:访问器属性是指那些通过 getter 和 setter 函数来定义的属性,它们并不存储具体的值,而是在每次访问时计算并返回一个值。由于原型链的特点,我们可以将访问器属性定义在对象的原型上,从而实现访问器属性继承。
- ES6 Class:在 ES6 中,我们可以通过 class 和 extends 关键字来实现类和继承。虽然底层实现还是基于原型和原型链,但语法更加简洁易用,也更符合面向对象编程的习惯。
总之,原型和原型链是 JavaScript 中的重要概念,掌握它们有助于提高代码的性能、灵活性和可维护性。
32、说说你对同步和异步的理解?
在编程中,同步和异步是两个非常重要的概念。简单来说,同步和异步是指代码执行的顺序。
- 同步:同步指的是代码依次执行,每段代码要等待上一个代码执行完成后才能继续执行。也就是说,在同步执行模式下,程序会一直等待某个操作完成,然后才会执行下一个操作。比如,当调用一个函数时,程序会阻塞在该函数处,直到函数返回结果才能继续往下执行。
- 异步:异步指的是代码不依次执行,而是同时执行多个操作,每个操作都有一个回调函数,当某个操作完成后,会回调对应的函数。换句话说,在异步执行模式下,程序不会等待某个操作完成,而是继续执行下一个操作。比如,当发起一个 Ajax 请求时,程序不会阻塞在该请求处,而是可以继续执行其他操作,当请求完成后,再执行对应的回调函数。
总之,同步和异步是针对代码执行顺序的概念,同步执行模式下代码按顺序执行,而异步执行模式下代码先执行一部分,再回调处理结果。在实际开发中,我们通常会选择使用异步模式来处理耗时操作,以避免线程阻塞和提高用户体验。
33、数组方法及使用场景
在 JavaScript 中,数组是一种常用的数据结构,用于存储和操作一组相关的数据。为了方便地对数组进行操作和处理,JavaScript 提供了许多内置的数组方法。下面介绍几个常见的数组方法及其使用场景:
- push 和 pop
push()
方法用于在数组末尾添加一个或多个元素,pop()
方法用于从数组末尾删除一个元素。这两个方法一般用于实现先进先出(FIFO)的队列。示例代码如下:
const queue = []; queue.push(1); // 添加元素 1 到队列末尾 queue.push(2); // 添加元素 2 到队列末尾 queue.pop(); // 删除队列末尾的元素 2
- shift 和 unshift
shift()
方法用于删除数组第一个元素,unshift()
方法用于在数组开头添加一个或多个元素。这两个方法一般用于实现后进先出(LIFO)的栈。示例代码如下:
const stack = []; stack.unshift(1); // 添加元素 1 到栈底 stack.unshift(2); // 添加元素 2 到栈底 stack.shift(); // 删除栈顶的元素 2
- concat
concat()
方法用于将多个数组合并成一个新数组。示例代码如下:
const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const newArr = arr1.concat(arr2); // 将 arr1 和 arr2 合并成一个新数组
- slice
slice()
方法用于从数组中选取一部分元素,并返回一个新数组,不会改变原数组。示例代码如下:
const arr = [1, 2, 3, 4, 5]; const newArr = arr.slice(1, 4); // 从索引为 1 的位置开始,选取三个元素,生成一个新的数组 [2, 3, 4]
- splice
splice()
方法用于向数组中插入或删除元素,并返回被删除的元素。示例代码如下:
const arr = [1, 2, 3, 4, 5]; arr.splice(2, 1); // 从索引为 2 的位置开始,删除一个元素,此时 arr 为 [1, 2, 4, 5] arr.splice(2, 0, 'a', 'b'); // 从索引为 2 的位置开始,插入两个元素 'a' 和 'b',此时 arr 为 [1, 2, 'a', 'b', 4, 5]
以上是常见的几个数组方法及其使用场景,除此之外,JavaScript 还提供了许多其他有用的数组方法,如 forEach、map、filter、reduce 等,需要根据实际需求来选择使用哪些方法。对于大型的数组操作,可以使用一些高效的第三方库来提高代码执行效率和开发效率。