JavaScript知识点总结

280 阅读27分钟

类型检测 & 快速区分

1. JS有几种基础数据类型?几种新增?*

JavaScript 中有七种数据类型,分别是:

  1. 数字(Number):整数或浮点数,使用 number 表示,例如 1、3.14 等。
  2. 字符串(String):由零个或多个字符组成的文本,使用 string 表示,例如 'Hello, world!'。
  3. 布尔值(Boolean):表示真或假,使用 boolean 表示,有两个值:true 和 false。
  4. 空值(Null):表示没有值,使用 null 表示,只有一个值:null。
  5. 未定义(Undefined):表示未定义的值,使用 undefined 表示,只有一个值:undefined。
  6. 对象(Object):由多个键值对组成的集合,使用 object 表示,例如 { name: '张三', age: 18 }。
  7. 符号(Symbol):表示独一无二的值,使用 symbol 表示。
  8. 大数字(BigInt):表示任意精度整数

2. 基础数据类型通常会进行分类,使用起来有什么区别?**

  • 原始数据类型:Number String boolean undefined Symbol BitInt
  • 引用数据类型:对象(Object)、数组(Array)、函数(Function)等

区别如下:

  1. 存储位置不同:原始数据类型直接存储在栈中,而引用类型的值存储在堆中,栈中存储的是对堆中数据的引用(地址)。
  2. 复制方式不同:原始数据类型的值在传递给函数或赋值给变量时是按值传递的,即复制一份新的值;而引用数据类型的值在传递给函数或赋值给变量时是按引用传递的,即复制一份引用,指向同一个对象。
  3. 可变性不同:原始数据类型的值是不可变的,一旦被创建,就不能被修改;而引用数据类型的值是可变的,可以随时被修改。

3. 如何进行类型区分?有几种类型判断的方式?*

  1. typeof:可以用来判断一个值的类型,返回一个表示该值类型的字符串。typeof 对于原始数据类型和 Function 类型的值是可靠的,但对于其他引用类型的值则不太可靠,例如对于 Array 类型,typeof 返回的是 "object" 而不是 "array"。示例代码如下:
console.log(typeof 123) // 输出 "number"
console.log(typeof 'hello') // 输出 "string"
console.log(typeof true) // 输出 "boolean"
console.log(typeof undefined) // 输出 "undefined"
console.log(typeof null) // 输出 "object"
console.log(typeof Symbol()) // 输出 "symbol"
console.log(typeof []) // 输出 "object"
console.log(typeof {}) // 输出 "object"
console.log(typeof function() {}) // 输出 "function"
  1. instanceOf:可以用来判断一个对象是否是某个类的实例,返回一个布尔值。instanceof 对于原始数据类型和 Function 类型的值不适用,只适用于判断对象是否是指定类的实例。示例代码如下:
const a = []
console.log(a instanceof Array) // 输出 true
console.log(a instanceof Object) // 输出 true
console.log(a instanceof Function) // 输出 false
  1. constructor:可以用来判断一个对象的构造函数是什么,返回一个构造函数。constructor 对于原始数据类型不适用,只适用于判断对象的类型。示例代码如下:
const a = []
console.log(a.constructor === Array) // 输出 true
console.log(a.constructor === Object) // 输出 false
console.log(a.constructor === Function) // 输出 false

  1. Object.prototype.toString:可以用来返回一个对象的字符串表示,包含对象的类型信息。示例代码如下:
console.log(Object.prototype.toString.call(123)) // 输出 "[object Number]"
console.log(Object.prototype.toString.call('hello')) // 输出 "[object String]"
console.log(Object.prototype.toString.call(true)) // 输出 "[object Boolean]"
console.log(Object.prototype.toString.call(undefined)) // 输出 "[object Undefined]"
console.log(Object.prototype.toString.call(null)) // 输出 "[object Null]"
console.log(Object.prototype.toString.call(Symbol())) // 输出 "[object Symbol]"
console.log(Object.prototype.toString.call([])) // 输出 "[object Array]"
console.log(Object.prototype.toString.call({})) // 输出 "[object Object]"
console.log(Object.prototype.toString.call(function() {})) // 输出 "[object Function]"

4. 手写实现一下instanceOf

function myInstanceOf(L, R) {
    var O = R.prototype;
    L = L.__proto__;
    function _instance(proto) {
        if (proto === null) {
            return false;
        }
        if (proto === O) {
            return true;
        }
        return _instance(proto.__proto__)
    }
    return _instance(L)
}

类型转换

1. isNaN 和 Number.isNaN 的区别?**

isNaN 是一个全局函数,用于判断给定的值是否是 NaN(Not A Number),如果是 NaN 则返回 true,否则返回 false。但是它的缺陷在于,如果传入的值不是数字类型,会先将其转换为数字类型,再进行判断。这就会导致一些奇怪的结果,如 isNaN('hello') 的结果是 true。

Number.isNaN 是一个静态方法,用于判断给定的值是否是 NaN,但是它不会将非数字类型的值转换为数字类型,而是严格判断。也就是说,如果传入的值不是数字类型,则直接返回 false。这样就避免了一些奇怪的结果,在判断时更加准确和安全。

2. 有没有其它的类型转换场景?***

  • 转换成字符串
    • Null => 'null'
    • Undefined => 'undefined'
    • Boolean => 'true' | 'false'
    • Number => '数字'
    • BigInt => 转成带有指数的形式
    • Symblo => '内容'
    • 普通对象 => '[object, Object]'
  • 转成数字
    • Undefined: NaN
    • Null => 0
    • Boolean: true => 1 | false => 0
    • String: 包含非数字的值 => NaN | 空字符串 => 0 | 纯数字字符串 => 对应的数字
    • Symbol: 报错
  • 转成Boolean
    • undefined | null | +0 -0 | NaN | '' => false

3. 原始数据类型如何操作属性?***

js中的原始数据类型是不可变的,不能在它们身上添加属性或方法。不过js中的每个原始数据类型都有一个对应的对象类型(包装类型),可以使用对象类型来操作原始数据类型的属性和方法。例如: 对于数字,可以使用 Number 对象来操作属性和方法:

const num = 123;
const numObj = new Number(123);

console.log(num.toFixed(2)); // 123.00
console.log(numObj.toFixed(2)); // 123.00

num.name = 'John';
numObj.name = 'John';

console.log(num.name); // undefined
console.log(numObj.name); // 'John'

对于字符串,可以使用 String 对象来操作属性和方法:

const str = 'hello';
const strObj = new String('hello');

console.log(str.toUpperCase()); // 'HELLO'
console.log(strObj.toUpperCase()); // 'HELLO'

str.name = 'John';
strObj.name = 'John';

console.log(str.name); // undefined
console.log(strObj.name); // 'John'

说说下面的执行结果

let a = new Boolean(false);
if (!a) {
    console.log('1231'); 
}
// 不会打印,因为new Boolean后会生成一个对象

数组操作的相关问题

1. 数组操作的基本方法?如何使用?

  • 转换方法:
    • toString() :将数组转换为一个字符串,并返回这个字符串

      const arr = [1, 2, 3];
      const str = arr.toString();
      console.log(str); // '1,2,3'
      

      需要注意的是,toString() 方法返回的字符串是由数组中的每个元素按照逗号连接而成的字符串。如果数组中的元素是对象,那么调用它们的 toString() 方法之后再连接成一个字符串返回。如果数组中的元素是 undefined 或 null,它们会被转换为空字符串。

      const arr = [1, undefined, {name: 'Bob'}, null, 3];
      const str = arr.toString();
      console.log(str); // '1,,[object Object],,3'
      
    • toLocaleString() toLocaleString() 是一个跟 toString() 类似的数组方法,它同样可以将数组转换为字符串并返回,但是它返回的字符串的格式是根据不同地区和语言环境而变化的。它可以将数组中的每个元素转换为本地字符串表示形式,并使用本地语言环境的分隔符将它们连接在一起。

      toLocaleString() 方法通常用于显示数组中包含的元素,比如日期对象,它可以将日期对象转换为本地日期格式。

      const arr = [
      new Date('2022-01-01'),
      new Date('2022-01-02'),
      new Date('2022-01-03')
      ];
      const str = arr.toLocaleString();
      console.log(str); // '1/1/2022, 1/2/2022, 1/3/2022'
      
    • join():将数组中的所有元素转换为字符串,并连接起来。

      const arr = [1, 2, 3];
      const str = arr.join('-');
      console.log(str); // '1-2-3'
      
  • 尾操作
    • pop():从数组末尾删除一个元素,并返回删除的元素。
      const arr = [1, 2, 3];
      const last = arr.pop();
      console.log(last); // 3
      console.log(arr); // [1, 2]
      
    • push():向数组末尾添加一个或多个元素,返回修改后的数组长度。
      const arr = [1, 2, 3];
      arr.push(4);
      console.log(arr); // [1, 2, 3, 4]
      
  • 首操作
    • shift():从数组开头删除一个元素,并返回删除的元素。
      const arr = [1, 2, 3];
      const first = arr.shift();
      console.log(first); // 1
      console.log(arr); // [2, 3]
      
    • unShift():向数组开头添加一个或多个元素,返回修改后的数组长度。
      const arr = [1, 2, 3];
      arr.unshift(0);
      console.log(arr); // [0, 1, 2, 3]
      
  • 排序
    • reverse():将数组中的元素位置颠倒,并返回颠倒后的数组。也就是说,它会改变原数组。

      const arr = [1, 2, 3];
      arr.reverse();
      console.log(arr); // [3, 2, 1]
      
    • sort():对数组中的元素进行排序,并返回排序后的数组。默认情况下,sort() 方法将数组元素看作字符串,将它们按照 Unicode 顺序进行排序。

      const arr = [2, 1, 3];
      arr.sort();
      console.log(arr); // [1, 2, 3]
      

      在排序时,sort() 方法会调用数组元素的 toString() 方法将它们转换为字符串,然后再进行比较。例如,对于数字类型的元素,它们会被转换为字符串后再进行比较,因此排序结果不一定符合我们的预期。

      如果想要按照自定义的方式对数组元素进行排序,可以传入一个比较函数作为参数。比较函数接受两个参数,分别是要比较的两个元素,它应该返回一个数字,表示它们的相对顺序。如果返回值小于 0,则表示第一个元素应该排在第二个元素的前面;如果返回值大于 0,则表示第一个元素应该排在第二个元素的后面;如果返回值等于 0,则表示它们的顺序不发生改变。

      例如,按照数字大小对数组进行排序:

      const arr = [2, 1, 3];
      arr.sort((a, b) => a - b);
      console.log(arr); // [1, 2, 3]
      
  • 连接
    • concat():返回一个新数组,由原数组和其他数组或值组合而成。
      const arr1 = [1, 2];
      const arr2 = [3, 4];
      const newArr = arr1.concat(arr2, 5);
      console.log(newArr); // [1, 2, 3, 4, 5]
      
  • 截取
    • slice():返回一个新数组,包含原数组指定的部分。
      const arr = [1, 2, 3, 4];
      const newArr = arr.slice(1, 3);
      console.log(newArr); // [2, 3]
      
  • 插入
    • splice():从数组中删除、替换或插入一个或多个元素。会改变原数组
      const arr = [1, 2, 3, 4];
      arr.splice(1, 2, 5, 6);
      console.log(arr); // [1, 5, 6, 4]
      
  • 索引
    • indexOf():返回指定元素在数组中的位置,如果不存在则返回 -1。
      const arr = [1, 2, 3, 2];
      const index = arr.indexOf(2);
      console.log(index); // 1
      
    • lastIndexOf():返回指定元素在数组中最后一次出现的位置,如果不存在则返回 -1。
      const arr = [1, 2, 3, 2];
      const index = arr.lastIndexOf(2);
      console.log(index); // 3
      
  • 迭代方法
    • every():every() 方法与 some() 方法类似,但它要求数组中的每个元素都必须满足指定的测试函数,才会返回 true。只要有一项测试返回 false,就会返回 false。
      const arr = [1, 2, 3];
      const result = arr.every(item => item % 2 === 0);
      console.log(result); // false
      
    • some():对数组中的每个元素执行指定的测试函数,只要有一项测试返回 true,就会返回 true。如果所有项测试都返回 false,则返回 false。
      const arr = [1, 2, 3];
      const result = arr.some(item => item % 2 === 0);
      console.log(result); // true
      
    • map():返回一个新数组,其中的元素是原数组经过指定函数处理后的结果。
      const arr = [1, 2, 3];
      const newArr = arr.map(item => item * 2);
      console.log(newArr); // [2, 4, 6]
      
    • forEach():对数组中的每个元素执行指定函数。
      const arr = [1, 2, 3];
      arr.forEach(item => console.log(item));
      // 1
      // 2
      // 3
      
    • filter():返回一个新数组,其中的元素是原数组经过指定函数过滤后的结果。
      const arr = [1, 2, 3, 4, 5];
      const newArr = arr.filter(item => item % 2 === 0);
      console.log(newArr); // [2, 4]
      
  • 归并
    • reduce():从左到右对数组中的所有元素进行累加计算,返回计算结果。
      const arr = [1, 2, 3, 4, 5];
      const sum = arr.reduce((total, item) => total + item, 0);
      console.log(sum); // 15
      

变量提升 & 作用域

1. 谈谈你对变量提升以及作用域的理解?*

  • 变量提升

    变量提升是 JavaScript 中的一种特殊的行为,它指的是在执行代码之前,JavaScript 引擎会先扫描整个代码块,将变量和函数声明提升到作用域的顶部,这个过程称为变量提升。 变量提升使得我们可以在声明变量之前就使用它,这是因为 JavaScript 引擎在编译代码时会将变量声明提升到作用域顶部,所以在变量声明之前使用变量并不会抛出错误。

  • 作用域

    是指代码中定义变量的区域,它决定了变量的可见范围和声明周期。在 JavaScript 中,每个函数都有自己的作用域,变量可以在当前作用域及其父级作用域中访问,但不能在子作用域中访问。

    • 全局作用域:是指在整个代码中都可以访问的作用域,它定义了全局变量和全局函数
    • 局部作用域: 指在函数内部定义的作用域,它定义了局部变量和局部函数。

在 JavaScript 中,变量的作用域是基于词法作用域的,也就是说,它是基于代码的位置来决定的,而不是基于代码的执行顺序。这意味着,如果在一个函数内部定义了一个变量,它在函数外部是不可见的,不管何时访问变量,都是访问到最近的定义该变量的作用域中的值。

例如,在下面的代码中,变量 x 在函数 foo() 内部定义,所以它的作用域仅限于函数 foo() 内部,在函数外部无法访问。

function foo() {
  var x = 10;
  console.log(x); // 输出 10
}
console.log(x); // 抛出错误,x 未定义

注意:let 和 const 声明的变量则不存在变量提升,var 关键字声明的变量会存在变量提升。

闭包

1. 什么是闭包?闭包的作用?*

闭包是指内部函数可以访问外部函数作用域中的变量,即使外部函数已经执行完毕,这些变量依然可以被内部函数访问。简单来说,闭包就是一个函数能够访问自由变量的能力。

在 JavaScript 中,每当函数被调用时,都会创建一个新的执行环境,并且会在作用域链中添加一个新的对象,用于存储局部变量和参数。当函数执行完毕后,这个执行环境会被销毁,作用域链中的对象也随之被释放。

但是,如果内部函数创建了一个闭包,它会持有外部函数作用域中的变量对象的引用,这个引用会一直存在,即使外部函数执行完毕,这些变量依然可以被内部函数访问。

闭包的作用:

  1. 实现公有变量

    闭包可以实现私有变量和私有方法,这些变量和方法对外部是不可见的。但是,如果我们想要实现公有变量,可以在外部函数中返回一个内部函数,这个内部函数可以访问外部函数中的变量,但又能被外部访问。

    function counter() {
    var count = 0;
    function increment() {
        count++;
        console.log(count);
    }
    return increment;
    }
    
    var c = counter();
    c(); // 输出 1
    c(); // 输出 2
    c(); // 输出 3
    

    上面代码中,外部函数 counter() 返回了一个内部函数 increment(),这个内部函数能够访问外部函数作用域中的变量 count,并且每次调用它都会将计数器加 1。

  2. 实现封装

    通过闭包,我们可以封装一些私有变量和方法,使得它们对外部不可见,从而提高代码的可维护性和安全性。

    var person = (function() {
    var name = '张三';
    return {
        getName: function() {
        return name;
        },
        setName: function(newName) {
        name = newName;
        }
    };
    })();
    
    console.log(person.getName()); // 输出 '张三'
    person.setName('李四');
    console.log(person.getName()); // 输出 '李四'
    

    上面代码中,我们使用立即执行函数创建了一个闭包,内部定义了一个私有变量 name 和两个公有方法 getName() 和 setName(),这两个方法可以访问和修改私有变量 name,但是对外部不可见。

    可以看到,通过闭包封装私有变量和方法,我们可以控制变量的访问权限,从而更好地保护程序的稳定性和安全性。

  3. 实现缓存

    闭包还可以用于实现缓存,比如,我们可以使用闭包来缓存一些计算结果,避免重复计算。

    function memoize(func) {
    var cache = {};
    return function() {
        var args = JSON.stringify(arguments);
        if (cache[args]) {
        return cache[args];
        } else {
        var result = func.apply(null, arguments);
        cache[args] = result;
        return result;
        }
    };
    }
    
    var fibonacci = memoize(function(n) {
    if (n === 0 || n === 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
    });
    
    console.log(fibonacci(10)); // 输出 55
    

    上面代码中,我们使用闭包实现了一个 memoize 函数,它接受一个函数作为参数,并返回一个新的函数。这个新的函数用于缓存函数的计算结果,如果下次参数相同,直接从缓存中取值,避免重复计算。

闭包的缺点

  1. 内存泄漏问题

    由于闭包会持有外部函数的变量的引用,如果这些变量占用的内存比较大,而闭包又没有被及时释放,就会导致内存泄漏问题。因此,在使用闭包时需要特别注意内存管理,及时释放不再需要的闭包,避免浪费内存。

  2. 性能问题

    由于闭包会持有外部函数的变量的引用,而这些变量又需要在内存中保存一份副本,所以会占用比较大的内存,从而影响性能。因此,在使用闭包时需要控制变量的作用域和生命周期,尽量减少不必要的闭包,避免影响程序的性能

ES6

1. const对象的熟悉可以修改吗?*

const 只能保证指针固定不变,指向的数据结构属性,无法控制是否变化。

2. new 一个箭头函数会发生什么?**

在 JavaScript 中,箭头函数与普通函数有着不同的实现方式和行为,因此在使用 new 关键字创建箭头函数时,会抛出一个 TypeError 错误,无法创建实例对象。

箭头函数不具有自己的 this 和 arguments,它们会从定义它们的作用域中继承。因此,在创建箭头函数时,会将它们的 this 绑定到定义它们的作用域中的 this,而不是绑定到新创建的对象上。

当我们使用 new 关键字创建一个普通函数时,会发生以下几个步骤:

  1. 创建一个新的对象
  2. 将这个新对象的原型指向构造函数的 prototype 属性
  3. 将构造函数的 this 绑定到这个新对象上,使 this 指向这个新对象
  4. 执行构造函数中的代码,初始化这个新对象的属性和方法
  5. 如果构造函数返回了一个对象,则返回这个对象,否则返回新创建的对象
// 代码实现 new 操作符
function myNew(fn, ...args) {
    // 创建一个新的对象
    const obj = {};
    // 将这个新对象的原型指向构造函数的 prototype 属性
    obj.__proto__ = fn.prototype;
    // 将构造函数的 this 绑定到这个新对象上,使 this 指向这个新对象
    const res = fn.apply(obj, args);
    // 果构造函数返回了一个对象,则返回这个对象,否则返回新创建的对象
    return typeof result === 'object' ? result : obj;
}

由于箭头函数没有自己的 this,所以无法绑定 this 到新创建的对象上,因此在使用 new 关键字创建箭头函数时,会抛出一个 TypeError 错误,无法创建实例对象。

3. JS,ES的内置对象有哪些?**

JavaScript 和 ECMAScript(ES)都定义了一些内置对象,其中一些是共享的,而其他一些则是特定于 JavaScript 或 ES 的。以下是一些常见的内置对象:

  1. 基本类型的包装对象:Boolean、Number、String、Symbol
  2. 全局对象:在浏览器中是 window,而在 Node.js 中是 global
  3. 全局函数对象:parseInt、parseFloat、isNaN、eval、encodeURI 等
  4. Math 对象:提供了数学相关的方法和常量
  5. Date 对象:提供了日期和时间相关的方法
  6. 正则表达式:RegExp 对象,用于处理字符串匹配和替换
  7. 数组:Array 对象,提供了许多与数组相关的方法
  8. Map 和 Set:ES6 引入的新集合类型
  9. Promise:ES6 引入的异步编程模型
  10. Proxy 和 Reflect:ES6 引入的元编程 API,用于在运行时修改 JavaScript 对象的行为

除此之外,还有 TypedArray、DataView、WeakMap、WeakSet、Symbol、Generator、Iterator 和 Atomics 等其他内置对象。

原型 & 原型链

1. 简单说说原型和原型链的理解?*

  • 原型

    当我们创建一个对象时,JavaScript引擎会默认为该对象创建一个原型对象(也称原型),并且与之关联。可以通过__proto__属性来访问对象的原型。

  • 原型链

    当我们访问对象的属性时,但当前对象本身并不存在该属性,JavaScript 引擎会在当前对象的原型上去查找,如果在原型上依然找不到,直到找到该属性或者原型链的顶端为止(注意:原型链的顶端是null)。像这样一层层向上查找形成的一个链条我们称之为原型链。

  • 构造函数

    JavaScript中创建一个对象是需要通过new 关键字调用 构造函数创建,所谓的构造函数其实和普通函数没有什么区别,只是在写法上稍有差别,一般构造函数是首字母大写。我们也可以通过构造函数的角度来解释原型和原型链,我们知道JavaScript中函数都有一个prototype属性(箭头函数除外),当我们通过构造函数 new 一对象时,那该对象的__proto__(原型)就会指向该构造函数的prototype上。下面通过一段代码和一张图解释下:

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    
    const person = new Person("张三", 18);
    console.log(person.__proto__ === Person.prototype); // true
    
    

继承

1. JavaScript中有哪几种继承方式?*

JavaScript中有以下几种继承方式:

  1. 原型链继承

    原型链继承是通过将子类的原型对象设置为父类的实例对象来实现继承。这种方式的缺点是原型对象是共享的,如果修改子类的原型对象,会影响到其它子类和父类的实例对象。

    代码实现:

    function Animal(name) {
        this.name = name;
    }
    
    function Dog(name, breed) {
        this.breed = breed;
    }
    
    Animal.prototype.sayName = funciton() {
        console.log(this.name);
    }
    
    Dog.prototye = new Animal();
    
    const dog = new Dog('Buddy', 'Labrador');
    dog.sayName(); // 'Buddy'
    
  2. 构造函数继承

    构造函数继承是通过在子类的构造函数中调用父类的构造函数,使用 call 或 apply 方法来实现继承。这种方式的缺点是父类的原型对象上的方法无法继承,每次创建子类的实例对象都需要重新执行一遍父类的构造函数。

    代码实现:

    function Animal(name) {
        this.name = name;
    }
    
    Animal.prototype.sayName = function() {
        console.log(this.name);
    };
    
    function Dog(name, breed) {
        Animal.call(this, name);
        this.breed = breed;
    }
    
    const dog = new Dog('Buddy', 'Labrador');
    dog.sayName(); // TypeError: dog.sayName is not a function
    
  3. 组合继承

    组合继承是通过将原型链继承和构造函数继承结合起来使用的继承方式。这种方式的优点是既可以继承父类原型对象上的方法,又可以继承父类构造函数中的属性。

    代码实现:

    function Animal(name) {
        this.name = name;
    }
    
    Animal.prototype.sayName = function() {
        console.log(this.name);
    };
    
    function Dog(name, breed) {
        Animal.call(this, name);
        this.breed = breed;
    }
    
    Dog.prototype = new Animal();
    Dog.prototype.constructor = Dog;
    
    const dog = new Dog('Buddy', 'Labrador');
    dog.sayName(); // 'Buddy'
    
  4. ES6 的类继承

    ES6 的类继承是通过使用 class 和 extends 关键字来实现继承。这种方式的优点是代码简洁易读,同时也支持 super 关键字来调用父类的构造函数和方法。

    代码实现:

    class Animal {
        constructor(name) {
            this.name = name;
        }
        sayName() {
            console.log(this.name);
        }
    }
    
    class Dog extends Animal {
        constructor(name, breed) {
            super(name);
            this.breed = breed;
        }
    }
    
    const dog = new Dog('Buddy', 'Labrador');
    dog.sayName(); // 'Buddy'
    

异步编程

1. JavaScript中有哪些异步编程方式?*

  1. 回调函数:回调函数是 JavaScript 中最基础的异步编程方式,通过将一个函数作为参数传递给另一个函数,在异步操作完成后执行回调函数来处理异步结果。
  2. Promise:Promise 是 ECMAScript 6 中新增的一个异步编程方式,它可以在异步操作完成后执行 then 方法中的回调函数,也可以通过 catch 方法来处理异步操作中的错误。
  3. Generator 函数:Generator 函数是 ECMAScript 6 中新增的一种异步编程方式,它可以在函数执行的过程中暂停和恢复执行,使得异步操作看起来像是同步操作一样。
  4. Async/await:Async/await 是 ECMAScript 7 中新增的一种异步编程方式,它可以通过使用 async 和 await 关键字来简化异步代码的书写,使异步代码看起来像是同步代码一样。

2. Promise 的理解?

Promise 是 ECMAScript 6 中新增的一种异步编程方式,它是一种表示异步操作的对象,可以通过 Promise 对象来处理异步操作的完成或失败。

Promise 有三个状态:

  1. Pending(进行中):Promise的初始状态
  2. Fulfilled(已完成):异步操作成功完成,Promise 对象的状态从 Pending 变为 Fulfilled。
  3. Rejected(已失败):异步操作失败或出错,Promise 对象的状态从 Pending 变为 Rejected。

Promise 的执行过程:

  • Promise 对象内部有一个 then 方法,可以接收两个回调函数作为参数,分别表示异步操作成功后的回调函数和异步操作失败后的回调函数。
  • 当 Promise 对象的状态从 Pending 变为 Fulfilled 时,会执行 then 方法中的第一个回调函数;
  • 当 Promise 对象的状态从 Pending 变为 Rejected 时,会执行 then 方法中的第二个回调函数。

Promise 的特点:

  1. 状态不可逆(只能从 Pending 到 Fulfilled 或 Pending 到 Rejected)
  2. 不能取消,一旦触发,中间是不能中断Promise的执行

Promise 对象的优点包括:

  1. 避免了回调函数嵌套的问题,可以使异步代码更加清晰和易于理解。
  2. 可以链式调用多个 then 方法来处理异步操作的完成或失败,提高代码的可读性和可维护性。
  3. 提供了 Promise.all 和 Promise.race 等方法来处理多个异步操作,使得异步编程更加灵活和高效。

3. 说说对 async/await 的理解?

async/await 是 ECMAScript 7 中新增的异步编程方式,它可以使异步代码看起来像是同步代码一样,提高了异步代码的可读性和可维护性。

async/await 的本质是基于 Promise 实现的,async 函数返回的是一个 Promise 对象,可以在函数体内使用 await 表达式来等待 Promise 对象的状态改变。当 await 表达式的 Promise 对象状态为 Fulfilled 时,它会返回异步操作的结果;当 Promise 对象状态为 Rejected 时,它会抛出异常。

async/await 的优点包括:

  1. 可以使用 try/catch 来处理异步操作中的错误,使得异步代码的异常处理更加方便和灵活。

  2. 可以使用同步的方式编写异步代码,使异步代码更加易于理解和维护。

  3. 可以方便地处理异步操作的结果,避免了回调函数的嵌套问题。

3. 手动实现下async方法?

要手写 async 函数的实现,需要先了解 async 函数的原理。async 函数本质上是基于 Generator 函数和 Promise 对象的封装,通过将异步操作封装成 Promise 对象来实现顺序执行。

下面是手写 async 函数的一个简单实现:

function async(fn) {
  return function() {
    const gen = fn.apply(this, arguments);

    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result;

        try {
          result = gen[key](arg);
        } catch (error) {
          return reject(error);
        }

        const { value, done } = result;

        if (done) {
          return resolve(value);
        } else {
          return Promise.resolve(value).then(
            (res) => step('next', res),
            (err) => step('throw', err)
          );
        }
      }

      return step('next');
    });
  };
}

在上面的代码中,我们定义了一个名为 async 的函数,该函数接收一个 Generator 函数作为参数,并返回一个新的函数。

在新的函数中,我们先执行 Generator 函数,得到一个迭代器对象 gen。然后,我们使用 Promise 对象来封装异步操作,当异步操作完成后,调用 resolve 方法来改变 Promise 对象的状态。在 Promise 对象的 then 方法中,我们使用 step 函数来顺序执行 Generator 函数中的下一个 yield 表达式。

在 step 函数中,我们首先执行 Generator 函数中的一个 yield 表达式,得到一个包含 Promise 对象的对象。如果该 Promise 对象已经完成或抛出异常,则使用 resolve 或 reject 方法来改变 Promise 对象的状态。否则,我们使用 Promise.resolve 方法来将 Promise 对象转换为普通值,并使用 then 方法来顺序执行下一个 yield 表达式。

在整个过程中,我们使用 try/catch 语句来捕获 Generator 函数中的异常,并使用 Promise 对象来封装异步操作,并在 then 方法中递归调用 step 函数,实现了 async 函数的顺序执行。

下面是一个使用手写 async 函数实现异步操作的示例:

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('Hello, world!');
    }, 1000);
  });
}

const asyncFetchData = async(function*() {
  const data = yield fetchData();
  console.log(data);
});

asyncFetchData();

在上面的示例中,我们定义了一个名为 fetchData 的异步函数,该函数返回一个 Promise 对象,在 1 秒后将返回 'Hello, world!'。然后,我们使用手写 async 函数异步地调用 fetchData 函数,并在异步操作完成后打印出异步操作的结果。

内存 & 浏览器执行相关

1. 简单说说对垃圾回收的理解?*

JavaScript 的垃圾回收机制是一种自动内存管理技术,通过检测不再使用的变量,自动释放这些变量所占用的内存空间,以避免内存泄漏和程序崩溃等问题。

2. 现代浏览器是如何处理垃圾回收的?**

JavaScript 的垃圾回收机制主要基于两个策略:标记清除和引用计数。

  • 标记清除:当一个变量进入执行环境时,会被加上标记,当变量离开执行环境时,会被清除标记。垃圾回收器会定期遍历所有对象,清除未被标记的对象。
  • 引用计数:垃圾回收器会对每个对象添加一个引用计数,当对象被引用时,引用计数加一,当对象不再被引用时,引用计数减一。当引用计数为 0 时,表示该对象已经没有任何引用,可以被垃圾回收器回收。

JavaScript 的垃圾回收机制在不同的浏览器中有所不同,主要取决于浏览器内核的实现方式。通常情况下,垃圾回收器会在内存占用过多或运行时间过长时主动执行垃圾回收操作,以释放不再使用的内存空间。因此,在编写 JavaScript 代码时,应尽量避免内存泄漏和不必要的变量引用,以提高程序的性能和稳定性。

EventLoop

1. 简单介绍下js的事件循环机制?

avaScript 的事件循环机制是用来处理异步操作的一种机制,它允许 JavaScript 在执行任务时不会阻塞主线程,而是将任务放到事件队列中,在主线程空闲时再去处理队列中的任务。

JavaScript 的事件循环机制包括以下几个部分:

  1. 调用栈(Call Stack):JavaScript 引擎中的调用栈用来维护函数的执行顺序。每当一个函数被调用时,该函数会被推入调用栈中,当函数执行完毕后,该函数会被弹出调用栈。

  2. 事件队列(Event Queue):事件队列用来存储异步操作的回调函数。当异步操作的回调函数准备好后,它会被推入事件队列中。包含两种:

    1. 微任务队列(Microtask Queue):微任务队列用来存储 promise 的 then 和 catch 方法的回调函数,以及 queueMicrotask 函数注册的回调函数。当调用栈为空时,会先处理微任务队列中的所有任务,然后再去处理事件队列中的任务。

    2. 宏任务队列(Macrotask Queue):宏任务队列用来存储 setTimeout、setInterval、setImmediate、requestAnimationFrame 等异步操作的回调函数。当调用栈为空且微任务队列为空时,会去处理宏任务队列中的任务。

事件循环机制的执行顺序为:首先,JavaScript 引擎会执行调用栈中的任务,当调用栈为空时,会去微任务队列中取出所有的任务,按照顺序执行所有的微任务,然后再去宏任务队列中取出一个任务,执行该任务,当宏任务队列为空时,重复上述过程。

总的来说,JavaScript 的事件循环机制允许 JavaScript 在执行异步操作时不会阻塞主线程,从而提高程序的性能和响应速度。了解事件循环机制的工作原理对于编写高效的异步代码非常重要。