JavaScript

为什么JS是单线程?

由于JS交互直接进行DOM操作,如果允许多线程对DOM进行并行操作,可能会导致竞态条件,例如一个线程正在读取节点,而另一个线程正在修改它。这将导致程序的不可预测性,因此,JS被设计为单线程语言,以避免这种复杂性。

node.js

node.js 用来做什么

NodeJs 是一个开源的JavaScript 运行环境和库,用于在客户的浏览器之外执行和运行网络应用,基于 V8 构建的引擎

Node.js 是一种基于 JavaScript 编程语言的后端开发工具,主要用于编写服务器端应用程序和网络应用。

它的主要优点是它的事件驱动和非阻塞 I/O 模型,这使得它在处理高并发和大量数据的情况下表现出色。Node.js 还内置了许多有用的模块和工具,包括文件系统操作、网络通信、数据流处理和加密等功能。

通过使用 Node.js,开发人员可以使用 JavaScript 编写服务器端代码,从而减少了前端和后端之间的语言转换成本,提高了开发效率。Node.js 还可以与前端框架(如 Angular 和 React)配合使用,从而实现全栈开发,使得开发人员可以使用相同的语言和技术栈开发整个 Web 应用程序。

node.js 是单线程还是多线程的

nodejs 不是多线程的,而是单线程的;nodejs 采用的是单线程异步非阻塞模式,因为 javascript 引擎的关系,node 默认的是单线程,一个 nodejs 应用无法利用多核资源,能够采用事件驱动和异步“i/o”的方式,实现一个单线程、高并发的运行时环境。

JS模块化

JavaScript 模块化是一种将代码分割成可重用单元(称为模块)的方法。这种方法提高了代码的可维护性、可重用性和可测试性。在 JavaScript 中,模块化的发展经历了几个阶段,从早期的简单对象封装,到现代的 ES6 模块(ECMAScript 2015 引入)和 CommonJS 规范等。

1. 早期模块化实践

在 ES6 之前的 JavaScript 中,没有内置的模块化支持。开发者们通常通过几种方式来实现模块化:

  • 立即执行函数表达式(IIFE):通过立即执行的函数(闭包)来封装变量和函数,避免污染全局作用域。
  • 对象字面量:将模块的方法和属性封装在一个对象内,通过对象字面量来模拟命名空间。
  • AMD(异步模块定义)和 RequireJS:AMD 是一种 JavaScript 模块定义规范,RequireJS 是一个实现了 AMD 规范的库,它允许你异步加载依赖模块。
  • CommonJS:Node.js 使用的模块化标准,它允许模块通过 require() 方法来同步加载依赖的其他模块,并通过 module.exportsexports 来导出模块接口。

2. ES6 模块化

ES6 引入了原生的模块化支持,这是 JavaScript 模块化发展的一个重要里程碑。ES6 模块使用 importexport 关键字来导入和导出模块。

  • 导出(Exporting):可以使用 export 关键字来导出模块中的函数、对象或原始值,以便其他模块通过 import 语句使用。

    javascript复制代码
    
    // 导出函数  
    export function myFunction() { ... }  
     
    // 导出变量  
    export const myVariable = ...;  
     
    // 导出多个  
    export { myFunction, myVariable };  
     
    // 导出默认成员  
    export default function() { ... }
    
  • 导入(Importing):使用 import 关键字来导入其他模块导出的内容。

    javascript复制代码
    
    // 导入单个成员  
    import { myFunction } from 'myModule';  
     
    // 导入多个成员  
    import { myFunction, myVariable } from 'myModule';  
     
    // 导入默认成员  
    import myDefault from 'myModule';  
     
    // 导入并重命名  
    import { myFunction as fn } from 'myModule';  
     
    // 导入全部成员  
    import * as myModule from 'myModule';
    

3. 模块化优势

  • 封装:模块只暴露必要的接口,隐藏内部实现细节,增强了代码的封装性。
  • 避免命名冲突:每个模块都有自己的命名空间,减少了全局命名冲突的风险。
  • 便于复用:模块可以被多个不同的程序或模块复用。
  • 易于维护:模块化的代码结构清晰,易于理解和维护。
  • 按需加载:现代模块打包工具(如 Webpack、Rollup)支持按需加载(懒加载)模块,提高了应用的加载速度和性能。

4. AMD和CommonJS的区别

AMD (Asynchronous Module Definition) 和 CommonJS 都是用于 JavaScript 的模块系统,但它们的设计目标和使用场景有所不同。下面是两者的主要区别:

定义和设计目标

  • CommonJS:

    • 主要为服务器端(如 Node.js)设计。
    • 模块加载是同步的。
    • 使用 require 来引入模块,使用 module.exportsexports 来导出模块。
  • AMD (Asynchronous Module Definition):

    • 主要为浏览器环境设计,支持异步加载模块。
    • 使用 define 函数来定义模块,可以指定依赖项并提供一个工厂函数或对象作为模块内容。
    • 加载器(如 RequireJS)会负责按需加载模块,并在所有依赖项加载完毕后执行模块代码。

语法示例

  • CommonJS:

    // 导入模块
    const fs = require('fs');
    const myModule = require('./myModule');
    
    // 导出模块
    module.exports = {
      someFunction: function() { /* ... */ }
    };
    
    // 或者
    exports.someFunction = function() { /* ... */ };
    
  • AMD:

    // 定义模块
    define(['dependency1', 'dependency2'], function(dep1, dep2) {
      return {
        someFunction: function() { /* ... */ }
      };
    });
    
    // 引入模块
    require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
      // 使用 moduleA 和 moduleB
    });
    

模块加载方式

  • CommonJS:

    • 在 Node.js 中,模块加载是同步的,这意味着当执行到 require 语句时,程序会暂停直到该模块被完全加载并执行完毕。
  • AMD:

    • 模块加载是异步的,这允许浏览器在加载模块的同时继续处理其他任务。这对于避免阻塞 UI 并提高用户体验非常有用。

使用场景

  • CommonJS:

    • 适合于服务器端的应用,特别是在 Node.js 环境中。
    • 由于其同步特性,它不适用于需要大量动态加载资源的客户端应用。
  • AMD:

    • 适合于浏览器环境,尤其是当你的应用需要动态加载多个脚本文件时。
    • 通过异步加载,它可以优化性能并提升用户体验。

兼容性

  • CommonJS:

    • 被 Node.js 社区广泛采用,大多数 npm 包都遵循 CommonJS 规范。
  • AMD:

    • 主要与 RequireJS 一起使用,尽管现在许多现代前端框架和构建工具(如 Webpack)已经内置了对 AMD 模块的支持或提供了更好的替代方案。

随着 ES6/ES2015 的发布,JavaScript 引入了原生的模块系统(importexport),这逐渐成为了新的标准。现代的打包工具(如 Webpack、Rollup 和 Parcel)通常支持将 CommonJS 和 AMD 转换为 ES 模块,以便在各种环境中都能良好工作。

6. ES6模块与CommonJS模块有什么异同?

ES6 Module和CommonJS模块的区别:

  • CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
  • import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。

ES6 Module和CommonJS模块的共同点:

  • CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。

数据类型

JavaScript 基本类型和引用类型的区别和注意点

  • 区别 1:基本数据类型的数据值存储在栈内存中,引用数据类型的数据值存储在堆内存中

  • 区别 2:变量复制时,基本数据类型复制的是数据值,引用数据类型复制的是地址值

    注意点 1: 切记:引用数据类型只有 new 时,才会在堆内存中开辟新的内存空间 引申:new 的过程,你真的理解了 new 的过程了吗?

    注意点 2: JavaScript 中的 String 类型是基本数据类型,在字符串调用字符串的方法时,系统自动创建一个临时字符串对象,当字符串调用完方法后,临时对象被销毁。

    String 类型比较特殊,其值不变性也要引起注意。

    值不变性:改变 String 类型数据值时,不像其他基本数据类型,在内存中直接修改数据值。而 Sting 类型修改数据值时,需 3 步: 1.首先需要重新开辟一块内存空间,并将原来的数据值复制一份到新的内存空间 2.然后在新的内存空间中,对数据值进行修改。 3.将变量指向新的内存空间的数据值。

    原来的内存空间没有了引用,系统会过一段时间将其回收。但具体过多久,就不知道了。所以应该避免大量对 String 类型数据值的修改。

    注意点 3: 1.引用数据类型变量的复制,只是将指向堆内存中的数据值的地址值复制了一份,两个地址值指向同一块堆内存。

    2.引用数据类型只有 new 时,才会在堆内存中开辟新的内存空间。

    3.对 String 类型数据值的修改,系统会开辟一块内存空间,并将数据值复制给新的内存空间,在新的内存空间中,对复制的数据值进行修改,变量重新指向新内存空间中数据值。

    原文链接:https://blog.csdn.net/baihehaitangyijiu/article/details/89228265

JavaScript 有哪些数据类型

JavaScript 共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

其中 Symbol 和 BigInt 是 ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

  • 在数据结构中,栈中数据的存取方式为先进后出
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区和堆区

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

为什么会有BigInt的提案?

JavaScript 中 Number.MAXSAFE_INTEGER 表示最⼤安全数字,计算结果是 9007199254740991(_2 的 53 次方 - 1),即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js 就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了 BigInt 来解决此问题。

null 和 undefined 区别

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

数据类型检测的方式有哪些

(1)typeof

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object

其中数组、对象、null 都会被判断为 object,其他判断都正确。

(2)instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false
console.log('str' instanceof String);                // false

console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

(3) constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true

(4)Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:

var a = Object.prototype.toString;

console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

同样是检测对象 obj 调用 toString 方法,obj.toString()的结果和 Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为 toString 是 Object 的原型方法,而 Array、function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串…),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString()不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。

判断数组的方式有哪些

  • 通过 Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通过原型链做判断
obj.__proto__ === Array.prototype;
  • 通过 ES6 的 Array.isArray()做判断
Array.isArray(obj);
  • 通过 instanceof 做判断
obj instanceof Array
  • 通过 Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

typeof null 的结果是什么,为什么?

typeof null 的结果是 Object。

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:

000: object   - 当前存储的数据指向一个对象。
  1: int      - 当前存储的数据是一个 31 位的有符号整数。
010: double   - 当前存储的数据指向一个双精度的浮点数。
100: string   - 当前存储的数据指向一个字符串。
110: boolean  - 当前存储的数据是布尔值。

如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。

有两种特殊数据类型:

  • undefined 的值是 (-2)30(一个超出整数范围的数字);
  • null 的值是机器码 NULL 指针(null 指针的值全是 0)

那也就是说 null 的类型标签也是 000,和 Object 的类型标签一样,所以会被判定为 Object。

instanceof 操作符的实现原理及实现

instanceof 操作符用于检测构造函数的 prototype 属性是否存在于某个对象的原型链上。其实现可以通过不断地检查对象的原型链,直到找到构造函数的 prototype 属性或者原型链终止为止。

function myInstanceof(obj, Constructor) {
  while (obj) {
    if (obj.__proto__ === Constructor.prototype) return true;
    obj = obj.__proto__;
  }
  return false;
}

// 使用示例
class A {}
class B {}

const a = new A();
console.log(myInstanceof(a, A)); // true
console.log(myInstanceof(a, B)); // false

Object.is() 与比较操作符 ===、== 的区别

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。(Object.is(+0, -0); // false)

如何判断一个对象是空对象

  • 使用 JSON 自带的.stringify 方法来判断:
if(Json.stringify(Obj) == '{}' ){
    console.log('空对象');
}
  • 使用 ES6 新增的方法 Object.keys()来判断:
if(Object.keys(Obj).length === 0){
    console.log('空对象');
}

对深拷贝浅拷贝的理解

在 JavaScript 中,理解深拷贝(Deep Copy)和浅拷贝(Shallow Copy)对于处理对象和数组等复合类型的数据至关重要。这两种拷贝方式的主要区别在于它们处理对象内部属性的方式。

浅拷贝(Shallow Copy) 浅拷贝只复制对象或数组的第一层属性,如果对象的属性值是基本数据类型(如 String、Number、Boolean、Symbol、Undefined、Null),则直接复制其值;但如果属性值是引用数据类型(如 Object、Array、Function 等),则只复制其引用地址,即两个对象指向同一块内存地址。因此,如果原对象内部属性的值发生变化,那么浅拷贝得到的新对象也会受到影响。

JavaScript 中常见的浅拷贝方法包括:

使用 Object.assign()方法(注意:Object.assign()会抛弃原对象的proto,即不会拷贝原型链)

使用展开运算符...(同样,也不会拷贝原型链)

使用数组的 slice()方法(只针对数组的第一层)

使用数组的 concat()方法(同上)

深拷贝(Deep Copy) 深拷贝会完整地复制一个对象,包括其所有属性(无论属性值是基本数据类型还是引用数据类型)和这些属性的属性值(如果属性值也是对象,则递归地复制该对象及其所有属性)。深拷贝后,原对象和新对象将完全独立,互不影响。

JavaScript 中并没有直接提供深拷贝的方法,但我们可以通过一些手段来实现深拷贝,比如:

使用 JSON.parse(JSON.stringify(obj))方法(注意:这种方法有一些局限性,比如不能复制函数、undefined、循环引用等

手动实现递归拷贝函数(可以处理更复杂的情况,包括函数、循环引用等)

示例

浅拷贝示例:

javascript
let original = { a: 1, b: { c: 2 } };
let shallowCopy = { ...original };

shallowCopy.a = 10; // 修改成功,互不影响
console.log(original.a); // 1

shallowCopy.b.c = 20; // 修改影响原对象
console.log(original.b.c); // 20

深拷贝示例(使用 JSON.parse(JSON.stringify(obj))):

javascript
let original = { a: 1, b: { c: 2 } };
let deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.a = 10; // 修改成功,互不影响
console.log(original.a); // 1

deepCopy.b.c = 20; // 修改不影响原对象
console.log(original.b.c); // 2

注意:深拷贝的实现方式多种多样,应根据实际需求选择最适合的方法。

object.assign 和扩展运算法是深还是浅拷贝,两者区别

扩展运算符:

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

Object.assign():

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

可以看到,两者都是浅拷贝。

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
  • 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制 ES6 的 symbols 属性。

0.1+0.2 为什么不等于 0.3

在 JavaScript 中,0.1 + 0.2 不等于 0.3 的问题源于浮点数的表示方式。计算机使用二进制来存储和处理数据,而某些十进制小数无法被精确地表示为有限的二进制小数。例如,十进制的 0.1 和 0.2 在转换成二进制时会变成无限循环的小数,这导致了精度损失。

当你执行 0.1 + 0.2 时,JavaScript 将这些数值转换为最接近的二进制表示形式,然后进行加法运算。由于这种近似,结果可能不会是精确的 0.3,而是非常接近的一个值,比如 0.30000000000000004

要解决这个问题,有几种方法可以确保比较或显示的结果符合预期:

方法 1: 使用 Number.EPSILON

你可以检查两个数字之间的差是否小于一个很小的阈值(如 Number.EPSILON),而不是直接比较它们是否相等。

function areEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}

console.log(areEqual(0.1 + 0.2, 0.3)); // true

方法 2: 四舍五入

你可以将结果四舍五入到一定的小数位数,以去除多余的尾数。

console.log((0.1 + 0.2).toFixed(1)); // "0.3"
// 注意 toFixed 返回的是字符串,如果需要数字类型,可以再转回来
console.log(Number((0.1 + 0.2).toFixed(1))); // 0.3

方法 3: 使用库

有些库专门用来处理高精度的数学计算,例如 decimal.jsbig.js。这些库可以在内部使用更高精度的算法来避免浮点数带来的误差。

const Decimal = require('decimal.js');

let a = new Decimal(0.1);
let b = new Decimal(0.2);

console.log(a.plus(b).toNumber()); // 0.3

方法 4: 整数运算

对于货币或其他要求精确计算的情况,一种常见的做法是将所有数值放大到整数(比如将金额乘以 100,这样就可以用整数来表示分),然后再进行计算,最后再除回去。

let a = 0.1 * 10; // 1
let b = 0.2 * 10; // 2

let result = (a + b) / 10; // 0.3
console.log(result); // 0.3

选择哪种方法取决于你的具体需求和场景。如果你只需要简单地显示结果,四舍五入可能是最简单的解决方案。如果你需要进行复杂的数学运算,并且对精度要求很高,那么使用专门的库可能是更好的选择。

对象操作的方法

在 JavaScript 中,有许多内置的方法可以用来操作对象。下面是一些常用的对象操作方法及其示例:

1. 创建对象

Object.create()
  • 用途:创建一个新对象,使用现有的对象来提供新创建对象的原型。

  • 语法Object.create(proto[, propertiesObject])

  • 示例

    const prototype = { greeting: 'Hello' };
    const obj = Object.create(prototype);
    console.log(obj.greeting); // 输出 'Hello'
    

2. 获取对象属性

Object.keys()
  • 用途:返回一个由一个对象自身的(可枚举)属性名组成的数组。

  • 语法Object.keys(object)

  • 示例

    const person = { name: 'John', age: 30 };
    const keys = Object.keys(person);
    console.log(keys); // 输出 ['name', 'age']
    
Object.values()
  • 用途:返回一个由一个对象自身的(可枚举)属性值组成的数组。

  • 语法Object.values(object)

  • 示例

    const person = { name: 'John', age: 30 };
    const values = Object.values(person);
    console.log(values); // 输出 ['John', 30]
    
Object.entries()
  • 用途:返回一个由一个对象自身的(可枚举)属性的 [key, value] 对组成的数组。

  • 语法Object.entries(object)

  • 示例

    const person = { name: 'John', age: 30 };
    const entries = Object.entries(person);
    console.log(entries); // 输出 [['name', 'John'], ['age', 30]]
    

3. 检查属性

hasOwnProperty()

  • 用途:检查对象是否具有指定的自身属性。

  • 语法object.hasOwnProperty(property)

  • 示例

    const person = { name: 'John', age: 30 };
    console.log(person.hasOwnProperty('name')); // 输出 true
    console.log(person.hasOwnProperty('city')); // 输出 false
    

4. 修改对象

Object.assign()
  • 用途:将所有可枚举的属性的值从一个或多个源对象复制到目标对象。

  • 语法Object.assign(target, ...sources)

  • 示例

    const source1 = { name: 'John' };
    const source2 = { age: 30 };
    const target = {};
    const result = Object.assign(target, source1, source2);
    console.log(result); // 输出 { name: 'John', age: 30 }
    

5. 删除对象属性

delete
  • 用途:删除对象的一个属性。

  • 语法delete object.property

  • 示例

    const person = { name: 'John', age: 30 };
    delete person.age;
    console.log(person); // 输出 { name: 'John' }
    

6. 遍历对象

#####for...in 循环

  • 用途:遍历对象的所有可枚举属性(包括继承的属性)。

  • 语法for (variable in object)

  • 示例

    const person = { name: 'John', age: 30 };
    for (const prop in person) {
      if (person.hasOwnProperty(prop)) {
        console.log(`${prop}: ${person[prop]}`);
      }
    }
    // 输出
    // name: John
    // age: 30
    

7. 深拷贝对象

JSON.parse()JSON.stringify()
  • 用途:创建一个对象的深拷贝。

  • 语法JSON.parse(JSON.stringify(object))

  • 示例

    const person = { name: 'John', age: 30 };
    const copy = JSON.parse(JSON.stringify(person));
    console.log(copy); // 输出 { name: 'John', age: 30 }
    

注意:这种方法不能深拷贝函数和循环引用的对象。

以上就是一些常用的 JavaScript 对象操作方法。您可以根据自己的需求选择合适的方法来处理对象。

ES6

ES6 新特性

1、let 和 const 声明变量的方式,取代了 var 关键字。

2、箭头函数(Arrow Function),简化了函数的书写方式。

3、模板字符串(Template String),允许在字符串中使用变量和表达式,而不需要使用字符串连接符号

4、解构赋值(Destructuring Assignment),允许从数组和对象中提取值并赋值给变量。

const arr = [1, 2, 3];
const [a, b, c] = arr;
console.log(a, b, c); // 1 2 3
const obj = {x: 1, y: 2, z: 3};
const {x, y, z} = obj;
console.log(x, y, z); // 1 2 3

5、默认参数(Default Parameter),在定义函数时可以给参数设置默认值。

6、扩展操作符(Spread Operator),可以在函数调用时展开数组或对象。

7、类(Class),引入了面向对象编程中类的概念。

8、模块化(Module),提供了一种组织代码的方式,可以将代码分割成独立的模块,方便重用和维护

9、Promise,用于处理异步操作,避免回调地狱的问题。

10、for…of 循环,用于遍历可迭代对象(如数组、Map 和 Set)中的元素。

11、Symbol,引入了一种新的数据类型,用于创建唯一的属性键。

12、Map 和 Set,引入了两种新的数据结构,分别用于存储键值对和唯一值。

13、Proxy,允许在对象和函数调用等操作前后添加自定义的行为。

14、Reflect,提供了一组可以操作对象的内置方法,可以替代一些对象方法(如 Object.defineProperty)的实现。

15、Promise.allSettled,用于处理多个 Promise 的状态并返回一个包含每个 Promise 状态的数组。

ES6 新增的声明方式:

es6: 1、let,用于声明变量,语法“let 变量名=值”;

2、const,用于声明常量,语法“const 常量名=值”;

3、class,用于声明类,语法“class 类名{...}”;

4、import,用于声明静态加载的输入变量。

es5:

1、var 声明变量

2、function:声明方法

let、const、var 的区别

console.log(a)
var a = 1
这段代码实际上等价于:
var a; // 变量提升
console.log(a); // 输出: undefined
a = 1; // 赋值
所以,console.log(a) 会输出 undefined

(1)块级作用域:let 和 const 具有块级作用域,var 不存在块级作用域

**(2)变量提升:**var 存在变量提升,let 和 const 不存在变量提升,即在变量只能在声明之后使用,否在会报错。

**(3)给全局添加属性:**浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。

**(4)重复声明:**var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const 和 let 不允许重复声明变量。

(5)暂时性死区:在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。

**(6)初始值设置:**在变量声明时,var 和 let 可以不用设置初始值。而 const 声明变量必须设置初始值。

**(7)指针指向:**let 和 const 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。

区别varletconst
是否有块级作用域×✔️✔️
是否存在变量提升✔️××
是否添加全局属性✔️××
能否重复声明变量✔️××
是否存在暂时性死区×✔️✔️
是否必须设置初始值××✔️
能否改变指针指向✔️✔️×

块级作用域

‌是指在代码块内定义的变量和函数只能在该代码块内被访问,超出该代码块后不可用。

块作用域由 { }包裹

块级作用域解决了 ES5 中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

变量提升

变量提升(Hoisting)是 JavaScript 中的一种行为,它将变量和函数的声明移动到其作用域的顶部。

暂时性死区

在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区

const 对象的属性可以修改吗

const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const 只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

es6 的 proxy 底层原理

ES6(ECMAScript 2015)中的Proxy对象的底层原理主要是基于 JavaScript 引擎对元编程和反射(reflection)的支持。以下是关于Proxy底层原理的详细解释:

  1. 拦截机制Proxy允许你创建一个对象的代理,从而可以拦截并修改对目标对象的某些操作。这是通过在Proxy构造函数中提供一个处理程序(handler)对象来实现的,该对象包含了一组陷阱(trap)函数。当对代理对象执行某些操作时(如读取属性、设置属性、函数调用等),JavaScript 引擎会首先检查处理程序对象中是否存在相应的陷阱函数。如果存在,则调用该陷阱函数,并将操作转发给它。
  2. 内部机制: 在 JavaScript 引擎内部,当创建一个Proxy对象时,引擎会为它分配一些内部属性,其中最重要的是[[ProxyTarget]][[ProxyHandler]][[ProxyTarget]]保存了被代理的目标对象,而[[ProxyHandler]]则保存了处理程序对象。这些内部属性在后续的操作中会被用来决定如何处理对代理对象的操作。
  3. 操作转发: 当对代理对象执行某个操作时,JavaScript 引擎会首先检查处理程序对象中是否存在相应的陷阱函数。如果存在,则调用该陷阱函数,并将操作转发给它。陷阱函数可以执行任何操作,包括调用目标对象的相应方法、修改参数、阻止操作等。如果陷阱函数没有定义或者返回了undefined(或者对于某些陷阱来说,返回了特定的“陷阱结果”),那么 JavaScript 引擎会使用默认行为。默认行为通常是调用目标对象的相应方法(如果存在的话)。
  4. 不变性和透明性Proxy对象本身是不可变的,你不能直接修改它的[[ProxyTarget]][[ProxyHandler]]。但是,你可以通过陷阱函数来修改目标对象的状态。此外,除了被陷阱函数拦截的操作外,Proxy对象应该尽可能地保持透明,即对于未被拦截的操作,它应该表现得和目标对象一样。
  5. 使用 Reflect 对象: 在编写陷阱函数时,建议使用 ES6 提供的Reflect对象。Reflect对象提供了一系列与对象操作相关的方法,这些方法与Object对象上的方法类似,但是它们的行为更加可靠和一致。通过使用Reflect对象,你可以更方便地对被代理对象进行操作,并且可以避免一些常见的错误。

总的来说,ES6 中的Proxy对象的底层原理是基于 JavaScript 引擎对元编程和反射的支持,通过拦截机制实现对目标对象操作的自定义和控制。

如果 new 一个箭头函数的会怎么样

箭头函数是 ES6 中的提出来的,它没有 prototype,也没有自己的 this 指向,更不可以使用 arguments 参数,所以不能 New 一个箭头函数。

new 操作符的实现步骤如下:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的proto属性指向构造函数的 prototype 属性)
  3. 指向构造函数中的代码,构造函数中的 this 指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

所以,上面的第二、三步,箭头函数都是没有办法执行的。

如何使用 for...of 遍历对象

for…of 是作为 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用 for..of 遍历是会报错的。

如果需要遍历的对象是类数组对象,用 Array.from 转成数组即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。

//方法一:
var obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
	var keys = Object.keys(this);
	var count = 0;
	return {
		next(){
			if(count<keys.length){
				return {value: obj[keys[count++]],done:false};
			}else{
				return {value:undefined,done:true};
			}
		}
	}
};

for(var k of obj){
	console.log(k);
}


// 方法二
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
    console.log(k,v);
}

JavaScript 基础

new 操作符的原理是什么

  • 在内存中自动创建一个新对象
  • 对象的原型指向构造函数的原型
  • 构造函数open in new window内部的this指向创建出来的新对象
  • 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型open in new window,就返回这个引用类型的对象。

map和set区别

MapSet 是 JavaScript 中两种不同的数据结构,它们各自有独特的用途和特性。下面是它们的主要区别:

Map

  • 键值对存储:
    • Map 对象保存键值对,并且任何类型的值(包括对象)都可以被用作键或值。
  • 初始化:
    • 可以通过传入一个二维数组来初始化,每个子数组都是一个 [key, value] 对。
  • 大小:
    • 可以使用 size 属性来获取 Map 中元素的数量。
  • 迭代:
    • 迭代时会按照插入顺序返回键值对。
  • 方法:
    • 提供了多种方法如 set(key, value), get(key), has(key), delete(key), clear() 等。

示例:

const myMap = new Map();
myMap.set('name', 'Alice');
myMap.set(1, 'one');

console.log(myMap.get('name')); // 输出: Alice
console.log(myMap.size);        // 输出: 2

Set

  • 唯一值集合:
    • Set 对象只允许存储唯一的值,不允许重复的值。
  • 初始化:
    • 可以通过传入一个数组或其他可迭代对象来初始化。
  • 大小:
    • 同样可以使用 size 属性来获取 Set 中元素的数量。
  • 迭代:
    • 迭代时会按照插入顺序返回值。
  • 方法:
    • 提供的方法如 add(value), has(value), delete(value), clear() 等。

示例:

const mySet = new Set();
mySet.add(1);
mySet.add(5);
mySet.add(5);  // 重复的值不会被添加

console.log(mySet.has(1)); // 输出: true
console.log(mySet.size);   // 输出: 2

主要区别

  • 键与值:
    • Map 存储的是键值对,而 Set 只存储值。
  • 键的类型:
    • Map 的键可以是任意类型,而 Set 的“键”就是其存储的值本身,因此它也必须是唯一的。
  • 使用场景:
    • 如果你需要关联两个值(例如用户 ID 和用户名),则应使用 Map
    • 如果你只需要存储一组不重复的值(例如一系列唯一的标签),则应使用 Set

性能考虑

  • 在大多数现代浏览器中,MapSet 都提供了比普通对象更好的性能,尤其是在处理大量数据时。
  • 当需要频繁地检查某个值是否存在时,Sethas 方法通常比数组的 indexOf 或者对象属性查找更快。

选择使用哪种数据结构取决于你的具体需求:如果你需要存储键值对并且键可以是任意类型,那么 Map 是合适的选择;如果你只需要存储一组唯一的值,那么 Set 会更简单且高效。

map 和 Object 的区别

MapObject
意外的键Map 默认情况不包含任何键,只包含显式插入的键。Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
键的类型Map 的键可以是任意值,包括函数、对象或任意基本类型。Object 的键必须是 String 或是 Symbol。
键的顺序Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。Object 的键是无序的
SizeMap 的键值对个数可以轻易地通过 size 属性获取Object 的键值对个数只能手动计算
迭代Map 是 iterable 的,所以可以直接被迭代。迭代 Object 需要以某种方式获取它的键然后才能迭代。
性能在频繁增删键值对的场景下表现更好。在频繁添加和删除键值对的场景下未作出优化。

JavaScript 脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • defer 属性:给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • async 属性:给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • 动态创建 DOM 方式:动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • 使用 setTimeout 延迟方法:设置一个定时器来延迟加载 js 脚本文件
  • 让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

什么是 DOM 和 BOM?

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口
  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。BOM 的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

对类数组对象的理解,如何转化为数组

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有这样几种:

  • 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
  • 通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
  • 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
  • 通过 Array.from 方法来实现转换
Array.from(arrayLike);

数组有哪些原生方法?

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
  • 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
  • 数组归并方法 reduce() 和 reduceRight() 方法

for...in 和 for...of 的区别

for…of 是 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for…in 的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

**总结:**for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

数组的遍历方法有哪些

方法是否改变原数组特点
forEach()数组方法,不改变原数组,没有返回值
map()数组方法,不改变原数组,有返回值,可链式调用
filter()数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用
for...offor...of 遍历具有 Iterator 迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的 obj 对象,将异步循环变成同步循环
every() 和 some()数组方法,some()只要有一个是 true,便返回 true;而 every()只要有一个是 false,便返回 false.
find() 和 findIndex()数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值
reduce() 和 reduceRight()数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作

遍历方法的详细解释:《细数 JavaScript 中那些遍历和循环》open in new window

判断空数组的方法

一、使用 length 属性

JavaScript 数组对象有一个 length 属性,可以用来获取数组的长度(即元素个数)。如果数组的 length 属性为零,表示数组为空数组

function isEmptyArray(arr) {

  return arr.length === 0;

}
使用示例:
const arr1 = []; // 空数组
console.log(isEmptyArray(arr1)); // 输出 true
const arr2 = [1, 2, 3]; // 非空数组
console.log(isEmptyArray(arr2)); // 输出 false

二、使用 Array.isArray() 方法

Array.isArray() 方法用于检查一个值是否为数组。结合 length 属性,可以用来判断数组是否为空数组。

function isEmptyArray(arr) {
  return Array.isArray(arr) && arr.length === 0;
}
 使用示例:
const arr1 = []; // 空数组
console.log(isEmptyArray(arr1)); // 输出 true
const arr2 = [1, 2, 3]; // 非空数组
console.log(isEmptyArray(arr2)); // 输出 false

三、使用 ! 取反运算符:

JavaScript 中,将一个数组视为布尔值时,空数组会被视为 false,非空数组会被视为 true。通过使用 ! 取反运算符,可以判断数组是否为空数组。

function isEmptyArray(arr) {
  return !arr.length;
}
使用示例:
const arr1 = []; // 空数组
console.log(isEmptyArray(arr1)); // 输出 true
const arr2 = [1, 2, 3]; // 非空数组
console.log(isEmptyArray(arr2)); // 输出 false

请注意,上述方法都是针对数组本身进行判断,而不涉及数组元素的内容。如果数组的元素虽然存在,但值为 null、undefined 或其他 "falsy" 值,仍然会被视为非空数组。如果需要考虑数组元素的内容,请在判断前进行适当的处理。

for循环和forEach哪个性能更好

性能差异

  • 内部机制forEach() 方法内部会创建一个新的作用域,并且它是一个函数调用,这意味着每次迭代都需要创建新的函数作用域和上下文。
  • 中断循环:使用 forEach() 无法直接中断循环(即没有内置的 breakcontinue),而 for 循环则可以。
  • 性能:由于上述原因,for 循环通常在性能上略胜一筹。然而,这种差异通常只在处理大量数据时才变得明显。

实践建议

  • 如果你关心性能并且要遍历的数组非常大使用 for 循环可能是更好的选择
  • 如果你需要更简洁的代码或者更简单的实现,forEach() 可以提供更好的可读性和简洁性。
  • 对于大多数日常编程任务而言,两者之间的性能差距并不重要。

forEach可以中断吗,如何中断foreach循环

在JavaScript中,forEach 循环本身并不提供直接的方法来中断循环(如 break 语句在常规循环中所做的那样)。forEach 是为数组设计的,用于遍历数组的每个元素并执行提供的回调函数,但它不提供提前退出的机制。

然而,有几种方法可以实现类似中断 forEach 循环的效果:

使用 Array.prototype.some(), Array.prototype.every(), 或 Array.prototype.find()

这些数组方法允许你提前退出循环,因为它们内部使用了短路逻辑。虽然它们的主要用途不是遍历所有元素,但你可以利用它们的特性来模拟中断 forEach 的行为。

  • some():当回调函数对任何元素返回 true 时,some 方法会立即返回 true 并停止遍历。
  • every():与 some() 相反,当回调函数对任何元素返回 false 时,every 方法会立即返回 false 并停止遍历。
  • find():返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

示例使用 some() 来中断:

javascript复制代码

const array = [1, 2, 3, 4, 5];  
  
array.some((item, index) => {  
  if (item === 3) {  
    console.log(`找到元素 ${item},索引为 ${index},中断循环`);  
    // 注意:这里不返回 true 或 false 不会影响循环的继续执行,  
    // 但我们可以利用这个特性来提前做一些处理  
    return true; // 或者根据需求返回 false  
  }  
  // 执行其他操作  
  console.log(item);  
  // 注意:如果这里不返回 true,some 会继续遍历数组  
});  
  
// 注意:some 实际上会返回布尔值,但在这个场景下我们主要用它来提前执行某些操作

使用传统的循环(如 forwhile

如果你需要更直接的控制循环的迭代和退出,可以使用传统的 forwhile 循环来替代 forEach

javascript复制代码

const array = [1, 2, 3, 4, 5];  
  
for (let i = 0; i < array.length; i++) {  
  if (array[i] === 3) {  
    console.log(`找到元素 ${array[i]},索引为 ${i},中断循环`);  
    break; // 使用 break 来中断循环  
  }  
  // 执行其他操作  
  console.log(array[i]);  
}

结论

虽然 forEach 不支持直接中断,但你可以通过其他数组方法或传统的循环结构来实现类似的功能。

map 和 forEach 的区别

map 方法返回一个新的数组,而 forEach 方法不会返回任何值,仅仅是遍历数组。 map 方法会遍历数组中的每个元素,并将每个元素传递给回调函数进行处理,然后将处理结果组成一个新的数组并返回。而 forEach 方法仅仅是遍历数组中的每个元素,不进行任何处理,因此不能生成新的数组。 map 方法会生成一个新的数组,而原数组不会被修改。而 forEach 方法不会生成新的数组,但是它可以修改原数组的值。 map 方法返回的数组长度与原数组相同,而 forEach 方法没有返回值。 下面是 map 和 forEach 方法的使用示例:

// 使用 map 方法将数组中的每个元素乘以 2 并生成一个新的数组
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(x => x * 2);
console.log(arr2); // 输出 [2, 4, 6, 8]
// 使用 forEach 方法打印数组中的每个元素
const arr3 = [1, 2, 3, 4];
arr3.forEach(x => console.log(x)); // 输出 1, 2, 3, 4

综上所述,map 方法适用于需要将原数组的每个元素进行处理并生成一个新的数组的场景,而 forEach 方法适用于仅仅需要遍历数组中的每个元素并对其进行操作的场景。

什么是防抖和节流?有什么区别?如何实现?

防抖(debounce) 是指在某个时间段内,只执行最后一次触发的函数调用。如果在这个时间段内再次触发该函数,会重新计时,直到等待时间结束才会执行函数。 这个技术通常用于处理频繁触发的事件,比如窗口大小调整、搜索框输入等。防抖可以避免函数执行过多次,以减少网络开销和性能负担。

节流(throttle) 是指在一段时间内限制函数的执行频率,保证一定时间内只执行一次函数调用。无论触发频率多高,都会在指定时间间隔内执行一次函数。 这个技术通常用于处理连续触发的事件,比如滚动事件、鼠标移动事件等。节流可以控制函数的执行频率,以减少资源消耗和提高性能。

区别

执行时机:防抖是在最后一次触发事件后的延迟时间内执行函数,而节流是在固定的时间间隔内执行函数。

而节流更适合限制频繁触发事件的执行频率。

防抖的应用场景包括:

  • 输入框搜索联想:用户连续输入时,延迟一定时间再发送请求,减少请求次数。
  • 窗口大小改变时的事件处理:用户调整窗口大小时,延迟一定时间再重新计算布局,避免频繁重绘。
  • 手机号验证码发送等

节流的应用场景包括:

- 页面滚动事件:滚动过程中触发的事件处理,可以通过节流来减少触发次数,提高性能。

- 鼠标移动事件:鼠标移动过程中触发的事件处理,可以通过节流来控制触发频率,避免过多的计算和渲染。

实现方式

防抖(Debounce)的实现:

防抖的实现通常依赖于 setTimeout 函数。当事件被触发时,首先清除之前的定时器(如果有的话),然后设置一个新的定时器,在延迟时间后执行事件处理函数。如果在延迟时间内事件被重新触发,那么之前的定时器会被清除,并重新设置一个新的定时器。这样,只有最后一次触发事件后的延迟时间内没有新的触发事件,事件处理函数才会被执行。

示例代码(使用 JavaScript):

javascript
function debounce(func, delay) {
 let debounceTimer;
 return function () {
 clearTimeout(debounceTimer);
 debounceTimer = setTimeout(() => {
 func.apply(this, arguments);
 }, delay);
 };
}

// 使用防抖函数
const searchInput = document.getElementById('searchInput');
const debouncedSearch = debounce(() => {
 console.log('执行搜索操作');
}, 500);
searchInput.addEventListener('keyup', debouncedSearch);


节流(Throttle)的实现:

节流的实现通常依赖于一个标志位(或称为“锁”)和一个定时器。当事件被触发时,首先检查标志位是否为可执行状态。如果是,则执行事件处理函数,并将标志位设置为不可执行状态,然后设置一个定时器,在延迟时间后将标志位重新设置为可执行状态。如果在延迟时间内事件被再次触发,由于标志位为不可执行状态,因此不会执行事件处理函数。

示例代码(使用 JavaScript):

javascript
function throttle(func, delay) {
 let canRun = true;
 return function () {
     if (canRun) {
        func.apply(this, arguments);
        canRun = false;
        setTimeout(() => {
            canRun = true;
        }, delay);
     }
 };
}

// 使用节流函数
const scrollEventHandler = throttle(() => {
 console.log('滚动事件触发');
}, 1000);
window.addEventListener('scroll', scrollEventHandler);

在实际开发中,根据具体的业务需求和场景选择合适的技术手段,可以有效地提高页面性能和用户体验。

匿名函数的典型应用场景是什么?open in new window

匿名函数可以在立即执行函数表达式( IIFE) 中使用,来封装局部作用域内的代码,以便其声明的变量不会暴露到全局作用域。

javascript

 代码解读
复制代码(function () {
  // 一些代码。
})();

匿名函数可以作为只用一次,不需要在其他地方使用的回调函数。当处理函数在调用它们的程序内部被定义时,代码具有更好地自闭性和可读性,可以省去寻找该处理函数的函数体位置的麻烦。

javascript

 代码解读
复制代码setTimeout(function () {
  console.log('Hello world!');
}, 1000);

匿名函数可以用于函数式编程或 Lodash(类似于回调函数)。

c

 代码解读
复制代码const arr = [1, 2, 3];
const double = arr.map(function (el) {
  return el * 2;
});
console.log(double); // [2, 4, 6]

你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?

柯里化(currying)是一种模式,其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。值得注意的是,对于需要被 curry 的函数,它需要从一个函数开始,然后分解成一系列函数,每个函数都需要一个参数。

function curry(fn) {
  if (fn.length === 0) {
    return fn;
  }

  function _curried(depth, args) {
    return function (newArgument) {
      if (depth - 1 === 0) {
        return fn(...args, newArgument);
      }
      return _curried(depth - 1, [...args, newArgument]);
    };
  }

  return _curried(fn.length, []);
}

function add(a, b) {
  return a + b;
}

var curriedAdd = curry(add);
var addFive = curriedAdd(5);

var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]

js字符串+0-0加0减0

在JavaScript中,对字符串进行加0或减0的操作通常是为了将字符串转换为数字。这种操作利用了JavaScript的类型转换特性。下面是这两种操作的具体说明:

字符串加0

当你对一个字符串加上数字0时,JavaScript会尝试将该字符串转换为数字。如果字符串可以被解析成一个有效的数字,则它会被转换为相应的数值;否则,结果将是NaN(Not-a-Number)。

let str1 = "42";
let num1 = str1 + 0; // 结果是数字 42

let str2 = "3.14";
let num2 = str2 + 0; // 结果是数字 3.14

let str3 = "hello";
let num3 = str3 + 0; // 结果是 NaN

console.log(num1, typeof num1); // 42 "number"
console.log(num2, typeof num2); // 3.14 "number"
console.log(num3, typeof num3); // NaN "number"

字符串减0

同样地,当你从一个字符串中减去数字0时,JavaScript也会尝试将该字符串转换为数字。这个过程与加0相同,只是使用了减法运算符。

let str1 = "42";
let num1 = str1 - 0; // 结果是数字 42

let str2 = "3.14";
let num2 = str2 - 0; // 结果是数字 3.14

let str3 = "hello";
let num3 = str3 - 0; // 结果是 NaN

console.log(num1, typeof num1); // 42 "number"
console.log(num2, typeof num2); // 3.14 "number"
console.log(num3, typeof num3); // NaN "number"

注意事项

  • 当字符串包含非数字字符时(除了前导和尾随的空白以及单个正负号),结果将是NaN
  • 如果字符串以0开头,并且后面跟着其他数字(例如"08"),则在某些情况下可能会导致八进制数的解析,但这仅限于旧版JavaScript(ECMAScript 5及更早版本)。在现代JavaScript(ES6+)中,这种行为已经被修正,这样的字符串会被直接解析为十进制数。

更安全的转换方法

虽然加0或减0的方法可以工作,但为了代码的可读性和健壮性,推荐使用内置的函数来转换字符串到数字:

  • parseInt(str, radix):将字符串解析为整数,第二个参数指定基数(如10表示十进制)。
  • parseFloat(str):将字符串解析为浮点数。
  • Number(str):尝试将整个字符串转换为数字,如果失败则返回NaN

这些方法提供了更明确的意图表达,并且在处理不同类型的数据时更加可靠。

js桥你是怎么理解的

在JavaScript开发中,"JS桥"通常是指一种机制或架构,它允许JavaScript代码与原生(Native)代码进行通信。这种通信可以是双向的,即从JavaScript调用原生方法,也可以是从原生环境调用JavaScript函数。JS桥主要用于混合应用(Hybrid App)开发中,比如使用Web技术构建的应用运行在原生容器内(如WebView),但需要访问设备特定的功能(例如摄像头、地理位置服务等),这些功能通常是Web API无法直接提供的。

JS桥的主要用途 访问原生功能:通过JS桥,开发者可以利用原生代码实现的功能,比如相机、联系人、硬件加速等。 性能优化:对于一些计算密集型任务或者动画,使用原生代码执行可能会比纯JavaScript更高效。 用户体验:原生组件通常能够提供更好的用户体验,特别是在用户界面方面。 安全性:某些敏感操作可能更适合在原生层面上处理,以增加安全性和隐私保护。 实现方式 JS桥的具体实现因平台而异,但基本原理相似:

iOS (Objective-C/Swift): 通过WKWebView或UIWebView来加载网页,并使用WKUserContentController或UIWebViewDelegate中的相关方法来处理JavaScript和原生之间的消息传递。 Android (Java/Kotlin): 使用WebView组件,并通过addJavascriptInterface方法向JavaScript暴露一个对象,该对象的方法可以从JavaScript中调用。同时,可以通过WebView的evaluateJavascript方法从原生端调用JavaScript函数。

定时器延时器区别

在JavaScript中,setTimeoutsetInterval 是两个用于定时执行代码的函数,但它们的工作方式和应用场景有所不同。

setTimeout

  • 用途setTimeout 用于设置一个延迟后执行一次特定的函数或代码片段。
  • 工作原理:当调用 setTimeout 时,它会等待指定的时间(以毫秒为单位)之后才执行提供的回调函数。一旦时间到了,回调函数会被调用一次,并且不会再次被调用。
  • 语法
    const timeoutId = setTimeout(function, delay, arg1, arg2, ...);
    
    • function:要执行的函数。
    • delay:延迟的时间(毫秒),即多久之后执行该函数。
    • arg1, arg2, ...:传递给函数的参数(可选)。
  • 清除定时器:可以使用 clearTimeout(timeoutId) 来取消尚未执行的 setTimeout

setInterval

  • 用途setInterval 用于每隔固定时间间隔重复执行特定的函数或代码片段。
  • 工作原理:当调用 setInterval 时,它会在每次达到指定的时间间隔时执行提供的回调函数,直到调用 clearInterval 或页面卸载为止。
  • 语法
    const intervalId = setInterval(function, delay, arg1, arg2, ...);
    
    • function:要定期执行的函数。
    • delay:时间间隔(毫秒),即每隔多久执行一次该函数。
    • arg1, arg2, ...:传递给函数的参数(可选)。
  • 清除定时器:可以使用 clearInterval(intervalId) 来停止定时器。

主要区别

  • 执行次数setTimeout 只执行一次,而 setInterval 会不断地重复执行,直到被手动停止。
  • 连续性:如果 setInterval 的执行时间超过了设定的时间间隔,那么下一次执行可能会出现堆叠。例如,如果一个任务需要500ms来完成,而你设置了每300ms执行一次,那么任务将不断累积,导致性能问题。相比之下,setTimeout 不会有这样的问题,因为它只执行一次。
  • 控制性setTimeout 更容易控制单次延迟后的执行时机,适合一次性操作;而 setInterval 适合需要周期性执行的任务,但需要注意可能引起的性能问题。

使用场景示例

  • setTimeout:用于实现如自动关闭弹出窗口、延迟加载某些资源等功能。

    setTimeout(() => {
      console.log('This will run after 2 seconds');
    }, 2000);
    
  • setInterval:用于实现如轮播图、实时更新数据、游戏中的定时刷新等功能。

    const intervalId = setInterval(() => {
      console.log('This will run every 2 seconds');
    }, 2000);
    
    // 在某个条件下停止定时器
    clearInterval(intervalId);
    

注意事项

  • 由于浏览器标签页可能进入非活动状态(例如用户切换到其他标签页),这可能导致 setTimeoutsetInterval 的执行暂停。在这种情况下,通常建议使用更现代的API如 requestAnimationFrame 或者监听 visibilitychange 事件来处理这些情况。
  • 当使用 setInterval 时,确保考虑到潜在的性能影响,尤其是在频繁执行较重的操作时。有时候,使用递归的 setTimeout 来替代 setInterval 会更加合适,这样可以更好地控制执行频率,避免任务堆积。

获取屏幕宽度

在Web开发中,获取屏幕宽度是一个常见的需求,可以用于响应式设计、布局调整以及其他需要根据屏幕尺寸进行变化的功能。你可以使用JavaScript来获取屏幕的宽度。以下是几种常用的方法:

1. 使用 window.innerWidth

window.innerWidth 属性返回浏览器窗口的视口(viewport)宽度,不包括滚动条。

const screenWidth = window.innerWidth;
console.log('屏幕宽度:', screenWidth);

2. 使用 document.documentElement.clientWidth

document.documentElement.clientWidth 返回文档的可见宽度,不包括滚动条。

const documentWidth = document.documentElement.clientWidth;
console.log('文档宽度:', documentWidth);

3. 使用 screen.width

screen.width 返回整个屏幕的宽度,包括任务栏等非浏览器部分。

const fullScreenWidth = screen.width;
console.log('全屏宽度:', fullScreenWidth);

4. 监听窗口大小变化

如果你需要在窗口大小发生变化时动态获取屏幕宽度,可以使用 resize 事件监听器。

function handleResize() {
  const screenWidth = window.innerWidth;
  console.log('当前屏幕宽度:', screenWidth);
}

// 添加事件监听器
window.addEventListener('resize', handleResize);

// 初始时也调用一次,以确保页面加载时能获取到正确的宽度
handleResize();

// 如果不需要了,记得移除事件监听器
// window.removeEventListener('resize', handleResize);

5. 在Vue.js中获取屏幕宽度

如果你使用的是Vue.js框架,可以在组件中通过生命周期钩子或计算属性来获取屏幕宽度。

使用生命周期钩子
<template>
  <div>
    <p>屏幕宽度: {{ screenWidth }}px</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      screenWidth: 0
    };
  },
  mounted() {
    this.updateScreenWidth();
    window.addEventListener('resize', this.updateScreenWidth);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.updateScreenWidth);
  },
  methods: {
    updateScreenWidth() {
      this.screenWidth = window.innerWidth;
    }
  }
};
</script>
使用计算属性

如果你只需要在模板中使用屏幕宽度,并且不需要存储它,可以使用计算属性。

<template>
  <div>
    <p>屏幕宽度: {{ screenWidth }}px</p>
  </div>
</template>

<script>
export default {
  computed: {
    screenWidth() {
      return window.innerWidth;
    }
  }
};
</script>

6. 在React中获取屏幕宽度

如果你使用的是React框架,可以通过类似的方式在组件中获取屏幕宽度。

使用状态和生命周期方法
import React, { useState, useEffect } from 'react';

function App() {
  const [screenWidth, setScreenWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setScreenWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      <p>屏幕宽度: {screenWidth}px</p>
    </div>
  );
}

export default App;

总结

以上是几种获取屏幕宽度的方法,选择哪种方法取决于你的具体需求和使用的前端框架。无论哪种方式,都可以帮助你实现基于屏幕宽度的响应式设计。

原型与原型链

1. 对原型、原型链的理解

理解 JavaScript 中的原型和原型链是掌握面向对象编程的重要部分。以下是对原型和原型链的详细解释:

原型 (Prototype)

JavaScript中 ,每个函数都有一个 prototype 属性,这个属性是一个对象,包含可以被该函数的所有实例共享的属性和方法。当你通过构造函数创建一个对象时,这个对象会继承构造函数 prototype 对象上的所有属性和方法。

原型链 (Prototype Chain)

原型链是指从一个对象的 __proto__ 属性开始,向上查找其原型对象,直到找到 null 为止的链式结构。每个对象都有一个内部属性 [[Prototype]],通常可以通过 __proto__ 属性访问。这个属性指向该对象的原型对象。

原型链的工作原理

当尝试访问一个对象的属性时,JavaScript 引擎会按照以下步骤进行查找:

  1. 自身查找:首先在对象自身的属性中查找。
  2. 原型查找:如果在对象自身中找不到,就沿着 __proto__ 链向上查找,直到找到该属性或到达原型链的终点(即 null)。

注意事项

  1. 修改原型对象:修改构造函数的 prototype 对象会影响所有已创建的实例。
  2. 原型继承:通过 Object.create 可以实现更灵活的原型继承。
  3. 性能考虑:频繁的原型链查找可能影响性能,因此在设计时应尽量减少深度嵌套的原型链。

在 JavaScript 中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

**特点:**JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

img

2. 原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到修改原型的时候 p 的构造函数不是指向 Person 了,因为直接给 Person 的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数 Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用 constructor 指回来:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

3. 原型链指向

p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person

4. 原型链的终点是什么?如何打印出原型链的终点?

由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__

img

5. 如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
}

6. 检测 一个属性是否在原型链上

在 JavaScript 中,你可以使用多种方法来检测一个属性是否存在于对象的原型链上。以下是几种常见的方法:

1. hasOwnProperty 方法

hasOwnProperty 方法用于检查对象自身是否具有指定的属性,不包括从原型链继承的属性。

const obj = { name: 'Alice' };
console.log(obj.hasOwnProperty('name')); // true

const proto = { age: 25 };
Object.setPrototypeOf(obj, proto);

console.log(obj.hasOwnProperty('age')); // false

2. in 运算符

in 运算符用于检查对象及其原型链中是否存在指定的属性。

const obj = { name: 'Alice' };
console.log('name' in obj); // true

const proto = { age: 25 };
Object.setPrototypeOf(obj, proto);

console.log('age' in obj); // true

3. Object.prototype.hasOwnProperty.call

如果你不确定对象是否实现了 hasOwnProperty 方法(例如,如果对象的原型被修改过),可以使用 Object.prototype.hasOwnProperty.call 来确保调用的是原生的 hasOwnProperty 方法。

const obj = { name: 'Alice' };
console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); // true

const proto = { age: 25 };
Object.setPrototypeOf(obj, proto);

console.log(Object.prototype.hasOwnProperty.call(obj, 'age')); // false

4. Object.getPrototypeOf 和递归检查

如果你需要逐层检查原型链上的每个对象,可以使用 Object.getPrototypeOf 并递归地检查每个原型对象。

function isPropertyInPrototypeChain(obj, prop) {
  let currentObj = obj;
  while (currentObj) {
    if (Object.prototype.hasOwnProperty.call(currentObj, prop)) {
      return true;
    }
    currentObj = Object.getPrototypeOf(currentObj);
  }
  return false;
}

const obj = { name: 'Alice' };
const proto = { age: 25 };
Object.setPrototypeOf(obj, proto);

console.log(isPropertyInPrototypeChain(obj, 'name')); // true
console.log(isPropertyInPrototypeChain(obj, 'age')); // true
console.log(isPropertyInPrototypeChain(obj, 'gender')); // false

5. Reflect.has 方法

Reflect.has 是 ES6 引入的一个静态方法,它的行为与 in 运算符相同,但可以作为函数调用,并且返回布尔值。

const obj = { name: 'Alice' };
console.log(Reflect.has(obj, 'name')); // true

const proto = { age: 25 };
Object.setPrototypeOf(obj, proto);

console.log(Reflect.has(obj, 'age')); // true

总结

  • hasOwnProperty:仅检查对象自身的属性。
  • in 运算符:检查对象及其原型链中的属性。
  • Object.prototype.hasOwnProperty.call:确保调用的是原生的 hasOwnProperty 方法。
  • Object.getPrototypeOf 和递归检查:逐层检查原型链上的每个对象。
  • Reflect.has:ES6 提供的静态方法,行为与 in 运算符相同。

根据你的具体需求选择合适的方法。如果你只需要检查对象自身是否具有某个属性,使用 hasOwnProperty;如果你需要检查整个原型链,使用 in 运算符或 Reflect.has。对于更复杂的场景,可以考虑递归检查原型链。

执行上下文/作用域链/闭包

1. 对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:

  • 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}

  • 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

闭包作为一种编程概念,尤其在JavaScript等语言中广泛应用,它允许一个函数访问另一个函数作用域中的变量。然而,闭包也存在一些问题,主要包括以下几个方面:

闭包存在的问题

一、内存泄漏
  1. 原因:闭包会保留对外部变量的引用,如果这些闭包没有被及时释放,那么这些变量也无法被垃圾回收机制回收。特别是当闭包的生命周期比较长,或者引用的外部变量比较大时,这个问题尤为突出。
  2. 示例
javascript复制代码

function foo() {  
    var name = 'foo';  
    var age = 22;  
    function stu() {  
        console.log(name);  
        console.log(age);  
    }  
    return stu;  
}  
var fc = foo();

在上述代码中,当fc函数被执行后,foo函数本身会被销毁。但由于stu函数引用了foo函数里的nameage,所以这两个变量不会被销毁。如果不进行适当处理,就会引起内存泄漏问题。

  1. 解决方案:在不再需要闭包时,将其置为null,从而允许垃圾回收机制回收其占用的内存。例如,在上述代码后加上fc = null;
二、性能损耗
  1. 原因:闭包的创建和执行需要额外的内存和时间开销,因为它需要保存函数及其相关的引用环境。
  2. 影响:过多的闭包可能会导致程序的性能下降,特别是在内存和处理能力受限的环境中。
三、代码复杂性增加
  1. 原因:闭包的使用可能会增加代码的复杂性,特别是在多层嵌套的情况下。
  2. 影响:这可能会使代码更难理解和调试,从而增加开发和维护的成本。
四、全局变量污染

虽然闭包本身是为了避免全局变量污染而设计的(通过创建私有作用域),但如果不当使用闭包(例如,在全局作用域中创建过多的闭包),也可能间接导致全局作用域被不必要的变量和函数污染,进而影响代码的可读性和可维护性。

综上所述,闭包虽然是一种强大的编程工具,但在使用时需要注意其潜在的问题。通过合理的设计和优化,可以最大限度地发挥闭包的优点,同时避免其带来的问题。

闭包应用场景

闭包在JavaScript中是一种强大的特性,它允许内部函数访问外部函数的变量和参数,即使外部函数已经执行完毕。这种机制为开发者提供了许多实用的应用场景。以下是闭包的一些常见应用场景:

1. 模拟私有变量

由于JavaScript没有提供直接支持私有成员的方式,可以使用闭包来创建私有变量。这些变量只能通过特定的方法进行访问或修改。

function createCounter() {
  let count = 0; // 私有变量

  return {
    increment: function() { count++; return count; },
    decrement: function() { count--; return count; },
    getCount: function() { return count; }
  };
}

const counter = createCounter();
console.log(counter.getCount()); // 0
counter.increment();
console.log(counter.getCount()); // 1

在这个例子中,count 变量是私有的,只能通过 increment, decrement, 和 getCount 方法访问或修改。

2. 事件处理

在事件处理中,闭包能够保持对数据的引用,使得每次事件触发时都能访问到正确的数据。这在处理循环生成的多个元素时特别有用。

for (let i = 0; i < 5; i++) {
  const button = document.createElement('button');
  button.innerText = `Button ${i + 1}`;
  button.addEventListener('click', (function(index) {
    return function() {
      console.log(`Button ${index} clicked`);
    };
  })(i));
  document.body.appendChild(button);
}

这里每个按钮点击时都会输出其对应的索引值,因为每个闭包都记住了当前的 i 值。

3. 函数工厂

利用闭包可以创建返回具有不同行为函数的工厂函数。这对于根据不同的输入生成具有特定功能的函数非常有用。

function makeMultiplier(multiplier) {
  return function(value) {
    return multiplier * value;
  };
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

makeMultiplier 返回一个新的函数,该函数“记住”了传入的 multiplier 参数。

4. 延迟执行

闭包可以帮助实现延迟执行某些操作的功能,比如定时器回调。

function delayedGreeting(name, delay) {
  setTimeout(function() {
    console.log(`Hello, ${name}!`);
  }, delay);
}

delayedGreeting('Alice', 1000); // 在1秒后打印 "Hello, Alice!"

这个例子中,setTimeout 的回调函数是一个闭包,它可以访问 name 参数。

5. 备忘录(Memoization)

备忘录技术用于缓存计算结果以提高性能。闭包非常适合实现这一功能,因为它能保留之前的计算结果。

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (!cache[key]) {
      cache[key] = fn.apply(this, args);
    }
    return cache[key];
  };
}

const fibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // 55

memoize 函数返回一个新函数,该函数会检查缓存中是否已存在计算结果,如果不存在则计算并存储起来。

6. 单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点。闭包可以帮助实现这一点。

const Singleton = (function() {
  let instance;

  function createInstance() {
    const object = new Object("I am the instance");
    return object;
  }

  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

在这个例子中,Singleton 是一个立即调用的函数表达式(IIFE),它创建了一个单例对象,只有第一次调用 getInstance 时才会初始化。

7. 柯里化(Currying)

柯里化是一种将接受多个参数的函数转换成一系列接受单一参数的函数的技术。闭包在此过程中起着关键作用。

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

curry 函数返回一个新函数,该函数累积参数直到达到原始函数所需的数量。

8. 防抖与节流

防抖(debounce)和节流(throttle)是两种常用的优化高频率触发事件(如窗口调整大小、滚动等)的技术。它们都依赖于闭包来保存状态。

// 防抖
function debounce(func, wait) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), wait);
  };
}

// 节流
function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

这两种技术都通过闭包来控制函数的执行频率,防止短时间内多次执行同一函数导致性能问题。

闭包的应用远不止这些,它是JavaScript编程中的一个重要概念,掌握了闭包可以让你写出更加灵活且高效的代码。

2. 对作用域、作用域链的理解

1)全局作用域和函数作用域

(1)全局作用域

  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有 window 对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

(2)函数作用域

  • 函数作用域指声明在函数内部的变量,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
  • 使用 ES6 中新增的 let 和 const 指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
  • let 和 const 声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链:

在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到 window 对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

3. 对执行上下文的理解

1. 执行上下文类型

(1)全局执行上下文

任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的 window 对象,并且设置 this 的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文

当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

(3)**eval**函数执行上下文

执行在 eval 函数中的代码会有属于他自己的执行上下文,不过 eval 函数不常使用,不做介绍。

2. 执行上下文栈
  • JavaScript 引擎使用执行上下文栈来管理执行上下文
  • 当 JavaScript 执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
//执行顺序
//先执行second(),在执行first()

3. 创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

1)创建阶段

(1)this 绑定

  • 在全局执行上下文中,this 指向全局对象(window 对象)
  • 在函数执行上下文中,this 指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段

此阶段会完成对变量的分配,最后执行完代码。

简单来说执行上下文就是指:

在执行一点 JS 代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为 undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出 this、arguments 和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

this/call/apply/bind

1. 对 this 对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。

  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。

  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。

  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。

    其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。

    call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。

    bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

  • 箭头函数没有自己的 this 绑定。箭头函数内的 this 值继承自包围它的函数或全局执行上下文。
var obj = {
    myMethod: function() {
        setTimeout(() => {
            console.log(this === obj); // true
        }, 0);
    }
};
obj.myMethod();

  • DOM 事件监听器

在 DOM 事件监听器中,this 通常指向触发事件的元素。

javascript复制代码

var button = document.getElementById('myButton');
button.addEventListener('click', function() {
    console.log(this === button); // true
});

  • 严格模式('use strict')

在严格模式下,如果函数被非对象调用(即全局调用或 null/undefined 调用),this 将是 undefined(而不是全局对象)。

'use strict';
function myFunction() {
    console.log(this === undefined); // true
}
myFunction();

理解 this 的行为是 JavaScript 中的一个重要概念,也是许多开发者初学时容易混淆的部分。通过了解不同的调用模式和 this 的绑定规则,你可以更好地控制函数中的 this 值。

2. call() 和 apply()和bind() 的区别?

它们的作用一模一样,区别仅在于传入参数的形式的不同。

  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
  • bind传入参数数量也不固定,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

3. 实现 call、apply 及 bind 函数

(1)call 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文对象的一个属性。
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性。
  • 返回结果。
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

(2)apply 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 将函数作为上下文对象的一个属性。
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性
  • 返回结果
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

(3)bind 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 保存当前函数的引用,获取其余传入参数值。
  • 创建一个函数返回
  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

4. 深入理解this的指向问题open in new window

一、this的指向主要有下面几种:

​ 1、this出现在全局函数中,永远指向window

var Car = function() {
    console.log(this); // window
    console.log(this.Car==window.Car,Car==window.Car); // true true
}
Car();

​ Car作为全局函数,直接调用时,函数的this指向window。

​ 也就是Car==window.Car===this.Car

​ 2、this出现在严格模式中 永远不会指向window

​ 函数中使用es5的严格模式‘use strict’,this为undefined

var Car = function() {
    'use strict'
    console.log(this); // undefined
}
Car();

​ 3、当某个函数为对象的一个属性时,在这个函数内部this指向这个对象

​ 如下图:在car这个对象的run方法中打印this为car本身。

var car = {
    name:'丰田',
    run() {
        console.log(this); // {name: "丰田", run: ƒ}
    }
}

​ 4、this出现在构造函数中,指向构造函数新创建的对象

[复制代码](javascript:void(0)😉

var Car = function(name) {
    this.name = name;
    console.log(this); // Car {name: "亚洲龙"}
                       // Car {name: "汉兰达"}
}
var myCar_1 = new Car('亚洲龙');
var myCar_2 = new Car('汉兰达');

[复制代码](javascript:void(0)😉

​ 上述代码中构造了两个实例对象myCar_1和myCar_2,构造函数里面的this分别指向创建的实例化对象myCar_1和myCar_2。

​ 5、当一个元素被绑定事件处理函数时,this指向被点击的这个元素

var btn = document.querySelector('button');
btn.onclick = function() {
    console.log(this); // <button>this</button>
}

​ 6、this出现在箭头函数中时,this和父级作用域的this指向相同

​ 接下来看一段比较复杂的代码:

[复制代码](javascript:void(0)😉

const obj = {
    Car() {
        setTimeout(function() {
            setTimeout(function() {
                console.log(this); // window
            })
            setTimeout(()=>{
                console.log(this); // window
            })
        })
        setTimeout(() => {
            setTimeout(function() {
                console.log(this); // window
            })
            setTimeout(()=>{
                console.log(this); // obj
            })
        })
    }
}
obj.Car();

[复制代码](javascript:void(0)😉

​ 看到上述代码是不是有点懵逼。别急 ! ! !,让我娓娓道来。

img

​ 首先遇到function(){}这种格式,如果是直接调用,浏览器会把Core传进去,所以①和③this指向为window。

​ 而箭头函数的this是指向父级作用域,➁的父级作用域是setTimeout(function(){})里面,

​ 前面已经说过function(){}这种格式如果直接调用Core传进去,this指向为window,

​ 所以➁的this指向=父级作用域的this指向=window,至于④就比较简单了,这里不做赘述。

二、修改this的指向

​ 上文说了this在函数中的指向问题,那么如何修改this的指向呢?

​ 1、使用call、apply、bind修改this指向

​ 具体使用方法请大家点击我另一篇博客 https://www.cnblogs.com/tsl0324/p/14556853.html

​ 3、使用new关键字改变this指向

​ 也就是上述第一大点的第三小点所举的例子。

5. 练习

var xx=1 
var obj= { 
	xx:3, 
	fun:function(){ 
		var xx=5; 
		return this.xx 
	}} 
var fun =obj.fun 
console.log(obj.fun(),fun())输出什么
//输出:
//3
//1

箭头函数

箭头函数与普通函数的区别

(1)箭头函数使用箭头(=>)来定义函数,语法更加简洁,省略了function关键字和大括号

  • 如果没有参数,就直接写一个空括号即可
  • 如果只有一个参数,可以省去参数的括号
  • 如果有多个参数,用逗号分割
  • 如果函数体的返回值只有一句,可以省略大括号
  • 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
let fn = () => void doesNotReturn();

(2)箭头函数没有自己的this

箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。

(3)箭头函数继承来的this指向永远不会改变

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a()  // undefined
new obj.b()  // Uncaught TypeError: obj.b is not a constructor

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'

(5)箭头函数不能作为构造函数使用

构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

(6)箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。如果需要访问传递给函数的参数,可以使用ES6引入的扩展运算符(...)来代替。‌

(7)箭头函数没有prototype

(8)箭头函数不能用作Generator函数,不能使用yeild关键字

箭头函数的this指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤,这个所谓的this也不会被改变。

可以⽤ Babel 理解⼀下箭头函数:

// ES6
const obj = {
  getArrow() {
    return () => {
      console.log(this === obj);
    };
  }
}

转化后:

// ES5,由 Babel 转译
var obj = {
   getArrow: function getArrow() {
     var _this = this;
     return function () {
        console.log(_this === obj);
     };
   }
};

JavaScript 基础

js的事件循环(Event Loop)

**总结:**JavaScript事件循环是一种处理异步事件和回调函数的机制,它是JavaScript实现异步编程的核心。事件循环中的任务分为同步任务和异步任务,同步任务是按照代码顺序依次执行的任务,异步任务是在任务队列中等待执行的任务,又可以分为宏任务和微任务。

宏任务有:

setTimeoutsetIntervalsetImmediate(仅在 Node.js 中)、I/O 操作UI 渲染事件处理 等。 微任务(Micro Task) 有: Promise.thenPromise.catchPromise.finallyMutationObserverprocess.nextTick(仅在 Node.js 中),await 后面的操作等。

微任务总是在当前宏任务结束后立即执行,优先级高于下一个宏任务。当一个宏任务中的所有微任务都执行完毕后,才会执行下一个宏任务,它会不断地从任务队列中取出任务并执行,直到任务队列为空为止。

1. 什么是js的事件循环

JavaScript事件循环是一种处理异步事件和回调函数的机制,它是JavaScript实现异步编程的核心。它在浏览器或Node.js环境中运行,用于管理任务队列和调用栈,以及在适当的时候执行回调函数。

2. 为什么会出现js的事件循环

JavaScript事件循环是为了解决JavaScript作为单线程语言时的并发性问题而设计的。由于JavaScript是单线程的,因此在执行代码时不能同时执行多个任务。这种单一线程的特性可能会导致JavaScript在处理某些长时间运行的操作(如网络请求、文件系统访问等)时出现阻塞,从而影响用户体验。 为了解决这些问题,JavaScript引入了异步编程模型和事件循环机制,它可以监听消息队列中的事件并根据优先级顺序依次执行相应的回调函数。这种机制允许JavaScript在等待某些操作完成的同时,可以执行其他任务,从而避免了阻塞,提高了效率和并发性,使得开发者可以使用异步编程模型来处理复杂的、长时间运行的操作,同时提供更好的用户体验。

4. 什么是宏任务和微任务?

  • 宏任务(Macro Task) 是 JavaScript 中执行的大块任务或代码块,它包括了一些常见的操作,如:

  • setTimeout

  • setInterval

  • setImmediate(仅在 Node.js 中)

  • I/O 操作

  • UI 渲染

  • 事件处理

  • 微任务(Micro Task) 是一个需要在当前宏任务完成后、下一个宏任务开始前立即执行的小任务。常见的微任务有:

    • Promise.thenPromise.catchPromise.finally
    • MutationObserver
    • process.nextTick(仅在 Node.js 中)

5. 事件循环的流程

事件循环中的任务分为两类:同步任务和异步任务。同步任务是按照代码顺序依次执行的任务,而异步任务则是在任务队列中等待执行的任务,例如定时器回调函数、事件回调函数和Promise回调函数等。异步任务又可以分为宏任务和微任务,微任务的执行优先级高于宏任务。当一个宏任务中的所有微任务都执行完毕后,才会执行下一个宏任务。事件循环的工作流程是不断地从任务队列中取出任务并执行,直到队列为空为止。

6. 宏任务与微任务的执行顺序

  • 微任务总是在当前宏任务结束后立即执行,优先级高于下一个宏任务。
  • 如果在微任务中再次添加微任务,这些新添加的微任务会在当前微任务队列完成后立即执行。

7. 事件循环与任务队列

JavaScript 的事件循环决定了宏任务和微任务的执行顺序。事件循环的基本流程如下:

  1. 执行全局代码:当 JavaScript 代码第一次运行时,首先会执行同步代码,这些代码被当作一个宏任务。
  2. 检查微任务队列:一旦宏任务完成,事件循环会检查并执行微任务队列中的所有任务,直到队列为空。
  3. 执行下一个宏任务:如果微任务队列为空,事件循环会从宏任务队列中取出下一个任务并执行。
  4. 重复上述步骤:这个过程会不断循环,直到所有任务执行完毕。

8. 事件循环的应用场景

  1. DOM 事件处理:通过监听 DOM 事件(例如 click、scroll 等),可以使用事件循环来异步更新 UI 或执行其他操作。
  2. 定时器:使用 setTimeout()setInterval() 函数可以创建定时器,用于在指定时间间隔之后执行相应的操作。这些操作会被作为异步任务添加到任务队列中等待执行。
  3. 网络请求:当 JavaScript 需要发送网络请求时,可以使用 XMLHttpRequestfetch API 发送异步请求,并将响应数据作为异步任务加入到任务队列中等待处理。
  4. Promise 和 async/await:Promise 和 async/await 是 JavaScript 中常用的异步编程方式,实际上它们底层都是基于事件循环机制实现的。通过将回调函数封装为 Promise 对象或 async 函数,可以让异步代码更加易读、易维护。
  5. Web Workers:Web Workers 可以让 JavaScript 在多线程环境下运行,从而避免阻塞主线程。Web Workers 使用了与事件循环类似的消息队列机制来实现异步通信。

9. js事件循环在浏览器那一步发生

浏览器的多进程的实现方式,主要的进程有三个:

1.浏览器进程(主要处理用户交互)

2.网络进程(主要处理网络请求)

3.渲染进程(主要负责解析 html css js 等页面渲染)

注意:目前谷歌浏览器的 一个标签页是一个单独的进程

事件循环发生于渲染进程中的渲染主线程中

10. 案例

console.log("start");
setTimeout(() => {
	console.log("timer");
}, 0);
console.log("end"); // 输出结果// start// end// timer
setTimeout(function () {
	console.log("setTimeout");
});

new Promise(function (resolve) {
	console.log("promise");
	for (let i = 0; i < 10000; i++) {
        if (i === 10) {
            console.log("for");
        }
        i == 9999 && resolve("resolve");
	}
}).then(function (val) {
	console.log(val);
});

console.log("console");

// promise   
//  for
//  console
//  resolve
//  setTimeout
async function async1() {
	console.log("async1 start");
	await async2();
	console.log("async1 end");
	setTimeout(() => {
    	console.log("timer1");
    });
}
async function async2() {
	setTimeout(() => {
		console.log("timer2");
	});
	console.log("async2");
}
async1();

setTimeout(() => {
	console.log("timer3");
});

console.log("start");  
//结果
//async1 start
//async2
//start
//async1 end
//timer2
//timer3
//timer1
async function a() {
	console.log("async-a");
	await b();
	console.log("async-b");
}
async function b() {
	console.log("async-b");
}
console.log("start");
setTimeout(() => {
	console.log("setTimeout-1");
}, 1000);
setTimeout(() => {
	console.log("setTimeout-2");
	new Promise((resolve, reject) => {
		console.log("setTimeout-promise");
		resolve("promise-1");
	}).then((res) => {
		console.log(res);
	});
}, 0);
new Promise((resolve, reject) => {
	console.log("promise");
	resolve("promise-2");
}).then((res) => {
	console.log(res);
});
a();
console.log("end");

// start
// promise
// async-a
// async-b
// end
// promise-2
// async-b
// setTimeout-2
// setTimeout-promise
// promise-1
// setTimeout-1
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

1
7
6
8
2
4
3
5
9
11
10
12
asyncfun = async () => {
  console.log('1');
  await asyncfun2();
  console.log('2');
}
function asyncfun2() {
  console.log('3');
}
console.log('4');
setTimeout(() => {
  console.log('5');
})
asyncfun();
new Promise((resolve) => {
  console.log('6');
  resolve();
}).then(() => {
  console.log('7');
  return new Promise(function (resolve) {
    resolve();
  })
}).then((res) => {
  console.log('8');
})
console.log('9');

4
1
3
6
9
2
7
8
5

数组操作

常用的数组操作方法

一、取最值

1.Math.min最小值

2.Math.max最大值

二、.length 获取当前数组的长度

三、includes 用来判断一个数组是否包含一个指定的值

四、join 方法用于把数组中的所有元素通过指定的分隔符进行分隔放入一个字符串,返回生成的字符串 默认用“,” 分隔

五、sliceopen in new window 可以截取某一个片段 浅拷贝

六、indexOf 获取某个元素在数组当中第一次出现的位置

七、lastIndexOf 获取某个字符最后出现在字符串的位置

八、concatopen in new window 方法用于合并两个或多个数组。不会更改现有数组,而是返回一个新数组。

九、push 表示向数组末尾进行追加 [会更改原数组]

十、unshift 表示向数组最前面进行追加 [会更改原数组]

十一、shift 删除数组的第一个元素,并返回这个元素。 [会更改原数组]

十二、pop 可以从数组的末尾开始删除 并且返回这个元素 [会更改原数组]

十三、sort 排序 [会更改原数组]

十四、forEach 对数组进行遍历循环,这个方法没有返回值

十五、reverse 可以将数组进行翻转 [会更改原数组]

十六、spliceopen in new window 删除,替换,插入 [会更改原数组]

十七、everyopen in new window 可以根据一个条件,判断当前数组是不是都满足这个条件

十八、some 只要有一个即可,可以根据一个条件,判断当前数组是不是有一个满足这个条件

十九、map 表示映射 对数组中的每个元素进行处理,返回新的数组 数组中的元素为原始数组元素调用函数处理后的值。

二十、filteropen in new window 过滤,方法创建给定数组一部分的浅拷贝open in new window,其包含通过所提供函数实现的测试的所有元素。

二十一、find 查找 -> 需要一个条件

二十二、findIndex 找某一个符合条件的索引

二十三、reduce 为数组提供累加器,合并为一个值

二十四、flat 数组扁平化处理

数组的遍历方法有哪些

方法是否改变原数组特点
forEach()数组方法,不改变原数组,没有返回值
map()数组方法,不改变原数组,有返回值,可链式调用
filter()数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用
for...offor...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环
every() 和 some()数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false.
find() 和 findIndex()数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值
reduce() 和 reduceRight()数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作

遍历方法的详细解释:《细数JavaScript中那些遍历和循环》open in new window

数组怎么转换字符串

在JavaScript中,将数组转换为字符串有几种常见的方法,每种方法都有其特定的用途和格式。以下是一些常用的方法:

使用 join() 方法

join() 方法可以将数组的所有元素连接成一个字符串,并以指定的分隔符(默认是逗号 ,)分隔这些元素。

let arr = ['Hello', 'world', 'this', 'is', 'great'];
let str = arr.join(); // 默认使用逗号作为分隔符
console.log(str); // 输出: "Hello,world,this,is,great"

// 指定分隔符
let strWithSpace = arr.join(' ');
console.log(strWithSpace); // 输出: "Hello world this is great"

使用模板字符串或字符串拼接

如果你需要更复杂的格式化,或者不想使用默认的分隔符,你可以使用模板字符串或简单的字符串拼接。

let arr = ['apple', 'banana', 'orange'];

// 使用模板字符串
let strTemplate = `${arr[0]}, ${arr[1]} and ${arr[2]}`;
console.log(strTemplate); // 输出: "apple, banana and orange"

// 字符串拼接
let strConcat = arr[0] + ', ' + arr[1] + ' and ' + arr[2];
console.log(strConcat); // 输出: "apple, banana and orange"

使用 JSON.stringify()

如果数组中的元素都是简单类型(如数字、字符串),并且你想要得到一个JSON格式的字符串,可以使用 JSON.stringify() 方法。

let arr = [1, 2, 3, 4, 5];
let jsonString = JSON.stringify(arr);
console.log(jsonString); // 输出: "[1,2,3,4,5]"

使用扩展运算符 ...toString()

扩展运算符 ... 可以用来展开数组,然后使用 toString() 将其转换为字符串。这与 join() 的默认行为相同。

let arr = [1, 2, 3, 4, 5];
let str = [...arr].toString();
console.log(str); // 输出: "1,2,3,4,5"

以上方法可以根据你的具体需求来选择。如果你只是简单地将数组元素连接成一个字符串,join() 是最直接的方式。如果你需要处理复杂的数据结构或者生成JSON格式的字符串,那么 JSON.stringify() 会是一个好选择。

es6数组去重有哪些方法

在ES6中,有多种方法可以用来去除数组中的重复元素。下面是一些常用的方法:

使用 Set

Set 是一个 ES6 中引入的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。因此,你可以将数组转换为 Set 然后再转回数组来去重。

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

使用 Array.from()Set

如果你需要兼容不支持扩展运算符(...)的环境,可以使用 Array.from() 方法。

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = Array.from(new Set(arr));
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

使用 filterindexOf

这种方法通过遍历数组并保留第一次出现的元素来实现去重。

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

使用 reduce() 和对象作为哈希表

利用 reduce 方法结合一个对象来追踪已经遇到过的元素。

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = arr.reduce((acc, item) => {
  if (!acc.includes(item)) {
    acc.push(item);
  }
  return acc;
}, []);
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

或者更简洁地使用一个对象来记录已经存在的元素:

let arr = [1, 2, 2, 3, 4, 4, 5];
let uniqueArr = arr.reduce((acc, item) => {
  if (!acc[item]) {
    acc[item] = true;
    return [...acc, item];
  }
  return acc;
}, []);
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

请注意,最后一种方法返回的是一个包含布尔值和数值的数组,因此通常我们会直接使用前几种方法。

这些方法各有优缺点,选择哪种取决于你的具体需求、代码可读性和性能考虑。如果只是简单地去除数字或字符串类型的重复项,使用 Set 通常是最快捷且最易读的方式。对于更复杂的情况,可能需要采用其他方法。

forEach 和map区别

  • forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
  • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

forEach如何中断

由于 forEach 方法本身不支持 break 语句,可以考虑使用抛出异常的方式来模拟 break。

在需要退出循环的地方,使用 throw new Error() 来抛出异常,并在循环外部使用 try…catch 块来捕获异常并处理退出循环的逻辑。但是,这种方式并不被推荐。

应该尽量使用其他循环语句如 for 或 while 来实现类似于 break 的功能。

1、使用 EVERYSOME 方法

2、还可以使用传统的 for 循环或 for...of 循环替代 forEach,这样就可以使用 break 来退出循环。

const arr = [1, 2, 3, 4, 5, 6, 7];

for (const item of arr) {
  console.log(item);
  
  if (item === 4) {
    break; // 终止循环
  }
}

3、在forEach回调函数内部使用return语句可以实现类似终止循环的效果。当需要终止循环时,可以在回调函数中返回false或者任意其他特定值。

const arr = [1, 2, 3, 4, 5, 6, 7];
let terminate = false;

arr.forEach((element) => {
  if (terminate) {
    return;
  }

  console.log(element);

  if (element === 3) {
    terminate = true; // 终止循环
  }
});

4、将数组长度设置成0

const array = [ 1, 2, 3, 4, 5, 6, 7 ]
array.forEach((item) => {
  if (item >= 4) {
    console.log(item) // 输出:4
    array.length = 0
  }
})

字符串

字符串转数组

在JavaScript中,将字符串转换为数组是一个常见的操作,有几种方法可以实现这一点。以下是其中的一些方法:

使用 split() 方法

split() 方法可以根据提供的分隔符将字符串分割成一个数组。如果不提供任何参数,则整个字符串会被当作单个元素存入数组。如果提供空字符串 '' 作为分隔符,则会将每个字符都作为单独的数组元素。

let str = "hello";
let arr = str.split('');
console.log(arr);  // 输出: ['h', 'e', 'l', 'l', 'o']

如果你想要根据特定的字符来分割字符串,你可以传递该字符给 split() 方法:

let str = "apple,banana,orange";
let arr = str.split(',');
console.log(arr);  // 输出: ['apple', 'banana', 'orange']

使用扩展运算符 ...

ES6 引入了扩展运算符 ...,它可以用来将字符串展开为一系列独立的字符,这些字符可以被收集到一个数组中。

let str = "hello";
let arr = [...str];
console.log(arr);  // 输出: ['h', 'e', 'l', 'l', 'o']

使用 Array.from()

Array.from() 方法可以从类数组对象或可迭代对象创建一个新的数组实例。当应用于字符串时,它会将每个字符作为一个元素放入数组。

let str = "hello";
let arr = Array.from(str);
console.log(arr);  // 输出: ['h', 'e', 'l', 'l', 'o']

以上三种方法都可以有效地将字符串转换为数组,选择哪种方法取决于你的具体需求和个人偏好。如果你只需要简单的字符拆分,那么 split('') 或者扩展运算符 ... 是很好的选择;如果你需要处理更复杂的逻辑(比如映射每个字符),则可能 Array.from() 会更适合。

字符串翻转

使用数组的 reverse 方法

你可以将字符串转换成数组,然后使用数组的 reverse 方法来翻转数组中的元素,最后再将数组转换回字符串。

function reverseString(str) {
    return str.split('').reverse().join('');
}

let originalStr = "hello";
let reversedStr = reverseString(originalStr);
console.log(reversedStr);  // 输出: olleh

这里,split('') 将字符串分割成单个字符组成的数组,reverse() 翻转数组中的元素顺序,而 join('') 则将这些字符重新组合成一个新的字符串。

使用循环

你也可以通过循环手动构建反转后的字符串:

function reverseString(str) {
    let reversed = '';
    for (let i = str.length - 1; i >= 0; i--) {
        reversed += str[i];
    }
    return reversed;
}

let originalStr = "hello";
let reversedStr = reverseString(originalStr);
console.log(reversedStr);  // 输出: olleh

在这个例子中,我们从字符串的最后一个字符开始遍历到第一个字符,并逐步构建新的反转字符串。

使用 ES6 的扩展运算符和 Array.from()

如果你喜欢更现代的 JavaScript 语法,可以使用 ES6 中的扩展运算符或者 Array.from() 函数结合 reverse 方法来实现:

function reverseString(str) {
    return [...str].reverse().join('');
}

// 或者
function reverseString(str) {
    return Array.from(str).reverse().join('');
}

let originalStr = "hello";
let reversedStr = reverseString(originalStr);
console.log(reversedStr);  // 输出: olleh

这两种方法都是先创建一个由字符串字符组成的数组,然后再执行与第一种方法相同的步骤。

以上就是在JavaScript中实现字符串翻转的一些基本方法。你可以根据具体的需求和个人偏好选择最合适的方法。

同步异步的理解

同步代码是指按照代码的顺序依次执行,每个代码块执行完之后才能执行下一个代码块。也就是说,同步代码是顺序执行的,必须等待前一个代码块执行完毕后才能执行下一个代码块。

异步代码是指不按照代码的顺序执行,而是在某个事件触发之后才会执行。也就是说,异步代码不会阻塞代码的执行,可以在等待某些操作完成的同时继续执行其他代码。

为什么会有同步和异步?

因为JavaScript的单线程,因此同个时间只能处理同个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验。 因此,JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务,因此,任务就可以分为同步任务和异步任务。

### 同步任务

同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。

异步任务

异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务

事件冒泡

异步编程

1. 异步编程的实现方式?

JavaScript 中的异步机制可以分为以下几种:

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

2. setTimeout、Promise、Async/Await 的区别

(1)setTimeout

console.log('script start')	//1. 打印 script start
setTimeout(function(){
    console.log('settimeout')	// 4. 打印 settimeout
})	// 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end')	//3. 打印 script start
// 输出顺序:script start->script end->settimeout

(2)Promise

Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

当 JS 主线程执行到 Promise 对象时:

  • promise1.then() 的回调就是一个 task
  • promise1 是 resolved 或 rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

(3)async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

例如:

async function func1() {
    return 1
}
console.log(func1())

img

func1 的运行结果其实就是一个 Promise 对象。因此也可以使用 then 来处理后续逻辑。

func1().then(res => {
    console.log(res);  // 30
})

await 的含义为等待,也就是 async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。await 通过返回一个 Promise 对象来实现同步的效果。

3. 对 Promise 的理解

Promise 是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

(1)Promise 的实例有三个状态:

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

当把一件事情交给 promise 时,它的状态就是 Pending,任务完成了状态就变成了 Resolved、没有完成失败了就变成了 Rejected。

(2)Promise 的实例有两个过程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

Promise 的特点:

  • 对象的状态不受外界影响。promise 对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是 promise 这个名字的由来——“承诺”;
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise 对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对 promise 对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

Promise 的缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
  • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

总结:

Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

**注意:**在构造 Promise 的时候,构造函数内部的代码是立即执行的

3.1. Promise 底层原理

promise 是一种用于处理异步操作的 javascript 对象,底层原理基于回调函数、事件监听和状态机等技术。在 promise 对象创建时,会初始化一个状态,通常有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝)。

当使用 promise 封装的异步操作成功完成时,promise 状态将变为fulfilled;如果发生错误,则状态将变为rejected。在这两种情况下,promise 都会触发相应的回调函数。

在 promise 代码中,通过使用 then()方法来添加回调函数。如果 promise 对象状态为fulfilled,则执行第一个回调函数;如果状态为rejected,则执行第二个回调函数。catch()方法用于捕获异常,并执行对应的回调函数。

promise 的优势在于可以解决回调地狱问题,避免嵌套的回调函数造成代码难以读取和维护的问题。同时,它还可以更好地控制异步操作的流程和结果,提高代码质量和可读性。

4. Promise 的基本用法

(1)创建 Promise 对象

Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一般情况下都会使用new Promise()来创建 promise 对象,但是也可以使用promise.resolve promise.reject这两个方法:

  • Promise.resolve

Promise.resolve(value)的返回值也是一个 promise 对象,可以对返回值进行.then 调用,代码如下:

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

resolve(11)代码中,会让 promise 对象进入确定(resolve状态),并将参数11传递给后面的then所指定的onFulfilled 函数;

创建 promise 对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建 promise 对象;

  • Promise.reject

Promise.reject 也是new Promise的快捷形式,也创建一个 promise 对象。代码如下:

Promise.reject(new Error(“我错了,请原谅俺!!”));

就是下面的代码 new Promise 的简单形式:

new Promise(function(resolve,reject){
   reject(new Error("我错了,请原谅俺!!"));
});

下面是使用 resolve 方法和 reject 方法:

function testPromise(ready) {
  return new Promise(function(resolve,reject){
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};
// 方法调用
testPromise(true).then(function(msg){
  console.log(msg);
},function(error){
  console.log(error);
});

上面的代码的含义是给testPromise方法传递一个参数,返回一个 promise 对象,如果为true的话,那么调用 promise 对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false的话,会调用 promise 对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks

(2)Promise 方法

Promise 有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。

1. then()

当 Promise 执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise 创建完了,那该如何调用呢?

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为resolved时调用,第二个回调函数是 Promise 对象的状态变为rejected时调用。其中第二个参数可以省略。

then方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此可以采用链式写法,即then方法后面再调用另一个 then 方法。

当要写有顺序的异步事件时,需要串行时,可以这样写:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{

})

那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。

2. catch()

Promise 对象除了有 then 方法,还有一个 catch 方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
);
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

3. all()

all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected

javascript
let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3]
})

调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个 promise 对象resolve执行时的值。

4. race()

race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
	console.log(res);
	//结果:2
},rej=>{
    console.log(rej)};
)

那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

5. finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:

promise
.finally(() => {
  // 语句
});
// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

5. Promise 解决了什么问题

解决了地狱回调的问题

在工作中经常会碰到这样一个需求,比如我使用 ajax 发一个 A 请求后,成功后拿到数据,需要把数据传给 B 请求;那么需要如下编写代码:

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

上面的代码有如下缺点:

  • 后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个 ajax 请求嵌套的情况,代码不够直观。
  • 如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。

Promise出现之后,代码变成这样:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data)
}).then(data=>{
  return read(data)
}).then(data=>{
  console.log(data)
})

这样代码看起了就简洁了很多,解决了地狱回调的问题。

6. Promise.all 和 Promise.race 的区别的使用场景

(1)Promise.all

Promise.all可以将多个Promise实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值

Promise.all 中传入的是数组,返回的也是是数组,并且会将进行映射,传入的 promise 对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。

需要注意,Promise.all 获得的成功结果的数组里面的数据顺序和 Promise.all 接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用 Promise.all 来解决。

(2)Promise.race

顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

7. 对 async/await 的理解

async/await 其实是Generator 的语法糖,它能实现的效果都能用 then 链来实现,它是为优化 then 链而开发出来的。从字面上来看,async 是“异步”的简写,await 则为等待,所以很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定 await 只能出现在 asnyc 函数中,先来看看 async 函数返回了什么:

async function testAsy(){
   return 'hello world';
}
let result = testAsy();
console.log(result)

img

所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda 表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

async function testAsy(){
   return 'hello world'
}
let result = testAsy()
console.log(result)
result.then(v=>{
    console.log(v)   // hello world
})

那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

8. await 到底在等啥?

**await 在等待什么呢?**一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test();

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

来看一个例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒钟之后出现hello world
  console.log('cuger')   // 3秒钟之后出现cug
}
testAwt();
console.log('cug')  //立即输出cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await 暂停当前 async 的执行,所以'cug''最先输出,hello world'和‘cuger’是 3 秒钟后同时出现的。

9. async/await 的优势

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

10. async/await 对比 Promise 的优势

  • 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是 then 的链式调⽤也会带来额外的阅读负担
  • Promise 传递中间值⾮常麻烦,⽽ async/await ⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await 可以⽤成熟的 try/catch,Promise 的错误捕获⾮常冗余
  • 调试友好,Promise 的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then 代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then 代码块,因为调试器只能跟踪同步代码的每⼀步。

11. async/await 如何捕获异常

async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

12.ES5怎么去实现异步

在 ES5 中,实现异步操作主要依赖于回调函数、Promise 和事件监听器。以下是几种常见的方法:

1. 回调函数

回调函数是 ES5 中最常用的异步处理方式。你可以在异步操作完成后执行一个回调函数来处理结果。

function fetchData(callback) {
  setTimeout(() => {
    const data = { name: 'John', age: 30 };
    callback(null, data); // 第一个参数通常用于传递错误信息
  }, 1000);
}

fetchData((err, data) => {
  if (err) {
    console.error('Error:', err);
  } else {
    console.log('Data:', data);
  }
});

2. 事件监听器

事件监听器可以用来处理异步操作的结果。例如,使用 EventEmitter(Node.js)或自定义事件系统。

// Node.js 示例
const EventEmitter = require('events');
const eventEmitter = new EventEmitter();

eventEmitter.on('data', (data) => {
  console.log('Data:', data);
});

setTimeout(() => {
  const data = { name: 'John', age: 30 };
  eventEmitter.emit('data', data);
}, 1000);

3. Promise

虽然 Promise 是在 ES6 中正式引入的,但在 ES5 中也可以通过库(如 es6-promise)来使用。

首先,安装 es6-promise 库:

npm install es6-promise

然后,在代码中使用 Promise:

require('es6-promise').polyfill();

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { name: 'John', age: 30 };
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log('Data:', data);
  })
  .catch(err => {
    console.error('Error:', err);
  });

4. 使用第三方库

还有一些第三方库可以帮助你更好地处理异步操作,例如 asyncQ

使用 async

async 库提供了许多有用的方法来处理异步操作,如 async.seriesasync.parallel 等。

首先,安装 async 库:

npm install async

然后,在代码中使用 async

const async = require('async');

async.waterfall([
  function(callback) {
    setTimeout(() => {
      callback(null, 'one', 'two');
    }, 1000);
  },
  function(arg1, arg2, callback) {
    console.log(arg1, arg2);
    callback(null, 'three');
  },
  function(arg1, callback) {
    console.log(arg1);
    callback(null, 'done');
  }
], (err, result) => {
  if (err) {
    console.error('Error:', err);
  } else {
    console.log('Result:', result);
  }
});
使用 Q

Q 是一个流行的 Promise 库,可以在 ES5 中使用。

首先,安装 Q 库:

npm install q

然后,在代码中使用 Q

const Q = require('q');

function fetchData() {
  var deferred = Q.defer();
  
  setTimeout(() => {
    const data = { name: 'John', age: 30 };
    deferred.resolve(data);
  }, 1000);

  return deferred.promise;
}

fetchData()
  .then(data => {
    console.log('Data:', data);
  })
  .catch(err => {
    console.error('Error:', err);
  });

总结

在 ES5 中,你可以通过回调函数、事件监听器、Promise(通过 polyfill 或第三方库)以及一些专门处理异步操作的库(如 asyncQ)来实现异步操作。选择哪种方法取决于你的具体需求和项目的复杂度。回调函数是最基本的方法,而 Promise 和第三方库则提供了更简洁和强大的异步处理能力。

13. promise.all 原理

Promise.all 是一个非常有用的方法,用于处理多个 Promise 实例,并在所有这些 Promise 都成功完成时返回一个包含它们结果的数组。如果任何一个 Promise 被拒绝(rejected),则 Promise.all 会立即返回一个被拒绝的 Promise,并带有第一个被拒绝的 Promise 的原因。

基本原理

Promise.all 的实现依赖于以下几点:

  1. 接收一个可迭代对象:通常是一个 Promise 数组。
  2. 创建一个新的 Promise:这个新的 Promise 会在所有传入的 Promise 都成功完成时解析,或者在任意一个 Promise 失败时拒绝。
  3. 跟踪完成状态:需要一个计数器来跟踪已经完成的 Promise 数量。
  4. 收集结果:需要一个数组来存储每个 Promise 的结果。
  5. 处理错误:一旦有 Promise 被拒绝,立即返回一个被拒绝的 Promise

手动实现 Promise.all

下面是一个简化的 Promise.all 实现,帮助理解其基本原理:

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }

    const results = [];
    let completedCount = 0;

    // 如果传入的是空数组,直接解析
    if (promises.length === 0) {
      return resolve(results);
    }

    promises.forEach((promise, index) => {
      // 确保每个元素都是一个 Promise
      Promise.resolve(promise).then(
        value => {
          results[index] = value;
          completedCount++;
          if (completedCount === promises.length) {
            resolve(results);
          }
        },
        reason => {
          reject(reason);
        }
      );
    });
  });
}

// 使用示例
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

promiseAll([p1, p2, p3])
  .then(result => console.log(result)) // 输出: [1, 2, 3]
  .catch(error => console.error(error));

详细解释

  1. 参数检查

    • 首先检查传入的参数是否是数组。如果不是数组,则抛出类型错误。
  2. 初始化

    • 创建一个空数组 results 用于存储每个 Promise 的结果。
    • 初始化一个计数器 completedCount 用于跟踪已经完成的 Promise 数量。
  3. 处理空数组

    • 如果传入的数组是空的,直接解析新的 Promise 并返回空数组。
  4. 遍历 Promise 数组

    • 使用 forEach 遍历每个 Promise
    • 对每个 Promise 使用 Promise.resolve 来确保它是一个真正的 Promise
    • 在每个 Promise 成功时,将结果存入 results 数组中,并增加 completedCount
    • 如果 completedCount 达到 promises.length,表示所有 Promise 都已完成,解析新的 Promise 并返回 results 数组。
    • 如果任意一个 Promise 被拒绝,立即拒绝新的 Promise 并传递拒绝的原因。

注意事项

  • Promise.all 只关心第一个被拒绝的 Promise,并且会立即返回一个被拒绝的 Promise
  • 如果传入的 Promise 数组中有非 Promise 对象,Promise.all 会使用 Promise.resolve 将其转换为 Promise
  • Promise.all 返回的 Promise 的结果数组顺序与传入的 Promise 数组顺序一致。

通过这种方式,Promise.all 提供了一种简洁的方式来处理多个异步操作,并且可以有效地管理它们的结果和错误。

面向对象

1. 对象创建的方式有哪些?

一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js 和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:

(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。

(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

2. 对象继承的方式有哪些?

(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。

(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用。

(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

垃圾回收与内存泄漏

1. 浏览器的垃圾回收机制

(1)垃圾回收的概念

垃圾回收:JavaScript 代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

回收机制

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
  • JavaScript 中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
  • 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

(2)垃圾回收的方式

浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。

1)标记清除

  • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

2)引用计数

  • 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减 1。当这个引用次数变为 0 时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
  • 这种方法会引起循环引用的问题:例如: obj1obj2通过属性进行相互引用,两个对象的引用次数都是 2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1obj2还将会继续存在,因此它们的引用次数永远不会是 0,就会引起循环引用。
function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

这种情况下,就要手动释放变量占用的内存:

obj1.a =  null
obj2.a =  null

(3)减少垃圾回收

虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • **对数组进行优化:**在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为 0,以此来达到清空数组的目的。
  • object**进行优化:**对象尽量复用,对于不再使用的对象,就将其设置为 null,尽快被回收。
  • **对函数进行优化:**在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

2. 哪些情况会导致内存泄漏

以下四种情况会造成内存的泄漏:

  • **意外的全局变量:**由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • **被遗忘的计时器或回调函数:**设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • **脱离 DOM 的引用:**获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • **闭包:**不合理的使用闭包,从而导致某些变量一直被留在内存当中。

3.什么是内存溢出和内存泄露

内存溢出

(或称为内存不足,Out of Memory, OOM)是指程序在运行过程中无法申请到足够的内存空间来继续执行,导致程序崩溃或抛出异常。

在前端开发中,内存溢出通常指的是浏览器分配给JavaScript运行环境的内存不足以支持当前页面的运行,导致页面崩溃或无法继续执行。这可能是因为:

  • 页面加载了过多的DOM元素或数据,占用了大量内存。
  • 使用了大量的全局变量或闭包,导致内存无法被垃圾回收机制回收。
  • 第三方库或框架的内存管理不当。
  • 长时间运行或复杂的JavaScript计算。

内存泄露(Memory Leak)

内存泄露是指程序中已分配的内存由于程序员的疏忽或错误而无法释放或回收,导致这些内存不再被程序使用但依旧占用着物理内存。随着运行时间的增加,这些未被释放的内存越来越多,最终可能导致内存溢出。

在前端开发中内存泄露则是指JavaScript程序中已经分配的内存由于某些原因无法被垃圾回收机制回收,导致这些内存持续占用,随着时间的推移,可用内存逐渐减少,最终可能导致内存溢出。内存泄露的常见原因包括:

  • 全局变量:无意中创建的全局变量(如未使用varletconst声明的变量)会在整个页面生命周期内持续存在。
  • DOM引用:如果JavaScript对象引用了DOM元素,并且这些DOM元素被从DOM树中移除,但JavaScript对象仍然持有它们的引用,那么这些DOM元素及其相关的内存就无法被回收。
  • 闭包:闭包可以保持对外部函数作用域中变量的引用,如果这些变量不再需要,但闭包仍然被引用,那么这些变量占用的内存也无法被回收。
  • 定时器或回调函数:如果定时器或回调函数引用了外部变量,并且这些变量在定时器或回调函数执行完毕后仍然被需要,那么这些变量占用的内存也无法被回收。

4.内存泄露的解决方法

  1. 避免全局变量:尽量使用letconst来声明局部变量,避免使用全局变量。
  2. 及时清理DOM引用:当DOM元素被移除时,确保相关的JavaScript对象也被清除或设置为null,以便垃圾回收机制可以回收这些内存。
  3. 注意闭包的使用:确保闭包不会无意中保持对外部变量的引用,特别是在闭包不再需要时。
  4. 管理定时器:使用clearTimeoutclearInterval来清除不再需要的定时器。
  5. 使用弱引用(如果可用):在某些情况下,可以使用弱引用来避免内存泄露,但请注意,JavaScript原生并不直接支持弱引用。
  6. 监控和分析:使用浏览器的开发者工具(如Chrome的DevTools)来监控内存使用情况,并查找内存泄露的源头。
  7. 代码审查和测试:定期进行代码审查和测试,确保没有引入新的内存泄露问题。
  8. 使用现代JavaScript框架和库:许多现代JavaScript框架和库都提供了内存管理的最佳实践和优化,使用它们可以减少内存泄露的风险。

设计模式

js设计模式有哪些

总体来说设计模式分为三大类:

  1. 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
  2. 结构型模式,共七种:适配器模式装饰器模式代理模式、外观模式、桥接模式、组合模式、享元模式。
  3. 行为型模式,共十一种:策略模式、模板方法模式、观察者模式/发布订阅模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

参考:https://juejin.cn/post/7330065707358208010#heading-44

Last Updated:
Contributors: 乙浒