夯实前端基础之JS篇

137 阅读17分钟

知识点概览

image.png image.png

一、数据类型 & 类型检测&类型转换

1. 数据类型种类

  • 8种数据类型Number,String,Boolean,Null, Undefined,Object, Symbol,BigInt
  • 新增的SymbolBigInt
    • Symbol:表示独一无二的值,主要用于对象属性名,避免属性名冲突。每个Symbol都是唯一的,可作为对象私有属性,不被一些遍历方法访问。
    const symb = Symbol("symb");
    const obj = {
       [symb]: "test hahah",
    };
    console.log(obj[symb]); // test hahah
    
    • BigInt:表示任意精度的整数,安全的存储和操作大数据,即便超出了Number安全整数范围
      • 使用场景:金融领域的精度计算,大数据处理,密码学,游戏开发中的资源计数(金币,经验值等)
    // 写法1:使用BigInt
    const bigData = BigInt(9007199254740991);
    // 写法2:使用n后缀
    const bigData2 = 2345678902345456782345n;
    
  • nullundefined有什么区别?
    • null:代表空值,它是被赋值的状态
    • undefined:表示一个变量声明了但还未被赋值,是一种未定义的状态
    • typeof时的区别:
      • 为什么typeof null为object:在 JavaScript 的早期实现中,它的值是用一个 32 位的单元存储的。前 3 位表示数据类型的信息,对于 null 值,它的机器表示全为0,所以被错误地判断为对象类型。
    console.log(typeof null); // "object" 
    console.log(typeof undefined); // "undefined"
    
    • 比较==和===时的区别:
    console.log(null == undefined); // true,JavaScript中一种特殊相等情况
    console.log(null === undefined); // false
    
  • 如何获取安全的undefined值?
    • 使用void 0void是一个元运算符,可以对任何表达式操作,然后返回undefined,最常用的还是void 0,主要用于需要返回undefined的特殊场景,比如在前面的HTML篇讲过的a标签
    <a href="javascript:void 0;">点击这里不会跳转</a>
    

2. 数据类型如何分类

  • 可以分为原始数据类型和引用数据类型
    • 原始:Number,String,Boolean,Null, Undefined, Symbol,BigInt
    • 引用:对象,数组,函数
  • 区别
    • 效果不同:
      • 原始数据类型直接赋值后,不存在引用关系
      • 引用数据属性存在引用关系,因为指针所指的堆内存一样,修改时会影响。
    • 存储位置不同
      • 原始:栈内存。变量的值直接存储在栈内存的相应位置 => 栈区由编译器自动分配释放 => 临时变量方式 => 空间小,大小固定,访问速度较快,操作频繁。
      • 引用:堆内存。对象本身存储在堆内存中,而在栈内存中会存储一个指向该对象在堆内存中位置的引用(指针) => 访问速度较慢 & 需要垃圾回收机制回收不再使用的对象所占用的内存空间.

3. 数据类型区分方式

常用4种:typeof,instanceof,constructor,Object.prototype.toString.call()

3.1 typeof方式:

typeof 123; // "number"
typeof "123"; // "string"
typeof true; // "boolean"
typeof {}; // "object" 
typeof []; // "object"   需要注意
typeof function(){}; // "function"
typeof Symbol(); // "symbol"
typeof undefined; // "undefined"

// 需要注意的特例
typeof null; // "object"
typeof NaN; // "number"

3.2 instanceof方式:

  • 检查一个对象是否是某个构造函数的实例,在判断对象的具体类型(尤其是自定义对象类型)以及处理继承关系时非常有用。
      123 instanceof Number; // false
      new Number(123) instanceof Number; // true
      [] instanceof Array; // true
      {} instanceof Object; // true
      new Date() instanceof Date; // true
      ...
    
  • 局限性:不能用于判断基本数据类型
  • 手写instanceof实现原理
    const myInstanceof = (left, right) => {
      // 1. 获取左边对象的原型,这是我们开始查找的起点
      let proto = Object.getPrototypeOf(left);
    
      // 2. 获取右边构造函数的prototype属性,这是我们要查找的目标原型
      const prototype = right.prototype;
    
      while (true) {
          // 如果对象的原型为null,说明已经到了原型链的末尾
          if (proto === null) {
              return false;
          }
    
          // 如果对象的原型和构造函数的prototype属性相等,说明在原型链上找到了
          if (proto === prototype) {
              return true;
          }
    
          // 继续往原型链上寻找
          proto = Object.getPrototypeOf(proto);
      }
      };
    
      myInstanceof([], Object);
    

3.3 constructor方式:

  • 通过访问对象的constructor属性,可以获取创建该对象的构造函数,然后将这个构造函数与已知的构造函数进行比较,从而判断对象的数据类型。
(123).constructor === Number; // true
({}).constructor === Object; // true
([]).constructor === Array; // true
...
  • constructor方式的隐患?
    • 原型链被修改的情况:如果对象的原型链被修改,constructor属性可能会指向错误的构造函数
    function MyObject() {}
    let myObj = new MyObject();
    console.log(myObj.constructor === MyObject); // true
    
    MyObject.prototype = {}; // 重写原型对象
    let anotherObj = new MyObject();
    console.log(anotherObj.constructor === MyObject); // false
    

3.4 Object.prototype.toString.call()方式:

  • 这个方法返回一个包含对象内部[[Class]]属性值的字符串,[[Class]]属性是一个内部属性,用于标识对象的类型。基本类型和引用类型都可以使用。
Object.prototype.toString.call(123); // "[object Number]"
Object.prototype.toString.call({}); // "[object Object]"     这个开发中可能常见
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(/123/); // "[object RegExp]"
...
  • 这里为啥要使用call
    • 改变this指向Object.prototype.toString() 本身是一个方法,其行为取决于 this 的值。使用 call 是为了将 this 指向需要判断类型的对象,为精确的类型判断提供了一个通用的解决方案,弥补了 typeof 等操作符在类型判断上的不足。
    • 为啥不是apply或者bind:apply需要一个额外的参数,bind需要多一步调用操作,繁琐,不适合
  • 为什么obj.toString()Object.prototype.toString.call(obj) 结果可能不同?
    • obj.toString() 调用的是对象自身或其原型链上的 toString 方法,这个方法可能已经被重写或有特定的功能,而 Object.prototype.toString.call(obj) 强制使用 ObjecttoString 方法并将 this 指向 obj,其目的是获取对象的类型信息。
    let arr = [1, 2, 3];
    console.log(arr.toString()); // "1,2,3"
    console.log(Object.prototype.toString.call(arr)); // "[object Array]"
    
  • 当对象中有某个属性和object的属性重名时,使用的顺序是什么样的?
    • 优先使用对象自身的
  • 如果说优先使用object属性,如何做?
    • 使用上述办法:Object.prototype.toString.call(obj),还比如Object.prototype.hasOwnProperty.call(arr, "length"); // true等原型上有的都可以。

3. 数据类型转换

3.1 isNaNNumber.isNaN的区别?

  • isNaN:包含一个隐式转换,它在判断之前会先尝试将传入的值转为数字类型
  • Number.isNaN:只会判断传入的值是否严格等于NaN,不会进行任何类型转换
// 传入NaN
console.log(isNaN(NaN)); // true
console.log(Number.isNaN(NaN)); // true

// 传入可以转换为数字的值
console.log(isNaN("123")); // false,隐式转换转为数字了
console.log(Number.isNaN("123")); // false, 不严格等于NaN
console.log(isNaN(true)); // false,隐式转换转为数字了
console.log(Number.isNaN(true)); // false, 不严格等于NaN

// 传入不可转换为数字的值
console.log(isNaN("abc")); // true,因为 'abc' 无法转换为数字,转换结果为 NaN
console.log(Number.isNaN("abc")); // false,'abc' 不严格等于 NaN

3.2 有哪些类型转换场景?

  • 3.2.1 隐式类型转换
    • 加法运算(+):当操作数中有一个是字符串时,另一个数字会转成字符串进行拼接
    • 其他运算(-,*,/,% ):操作数会被转换为数字类型
    • 相等比较(==):会进类型类型转换后再进行值比较
    • 逻辑运算(&&, ||, !):操作数会被转换为布尔类型
  • 3.2.2 显式类型转换
    • 转换为数字:Number(), parseInt(), parseFloat()
      • undefined => NaN
      • Null => 0
      • Boolean => true: 1 | false: 0
      • String => 包含非数字的值:NaN | 空: 0
      • Symbol => 报错
      • 对象 => 相应的基本值类型 => 相应的转换
    • 转换为字符串:String(), toString(),下面的是针对于String()方式的结果
      • Null, Undefined => 'null' , 'undefined'
      • Boolean => 'true', 'false'
      • Number => '数字' | 大数据 => 会转换成带有指数形式
      • Symbol => '内容'
      • 普通对象 =>'[0bject 0bject]'
    • 转换成布尔类型:Boolean()
      • undefined | null | +0, -0 | false | '' => false

3.3 原始数据类型如何具有属性操作的?

  • 包装对象机制
    • 当原始数据类型的值调用属性或者方法时,js会在后台隐式的将基本类型转换成对象
    // 字符串类型调用方法
    let str = "hello";
    console.log(str.toString());
    
    // js在后台的三步操作
    // 1. 创建一个 String 包装对象实例
    let tempStrObj = new String(str);
    // 2. 在这个实例上调用 toUpperCase 方法
    let upperCaseStr = tempStrObj.toUpperCase();
    // 3. 销毁临时创建的实例
    tempStrObj = null;
    console.log(upperCaseStr); // 输出: HELLO
    
  • 以下代码的执行结果是什么?
let a = new Boolean(false); // 输出:[Boolean: false]
if (!a) {
    console.log(a);
}
// never print. 原因:使用new Boolean构造函数返回的是一个对象,对象在js中无论内部什么值都会被视为真值

二、原型&作用域 & 上下文 & 闭包

1. 面向对象

1.1 什么是面向对象?本质是什么?有什么优势?

  • 定义
    • OOP,一种编程思想,利用类和对象,将现实世界的事物抽象为对象,通过'属性'和'方法'描述其状态和功能
    • 核心:封装,继承,多态,抽象
    • 典型案例:.vue文件为什么vue会认识,因为他们是从new Vue的模板中生成的
  • 本质
    • js对象的本质并不是直接基于类,而是基于 构造函数+原型链传递方式 => constructor + prototype
  • 优势
image.png

1.2 构造函数怎么工作的?constructor是什么?存在的意义?

  • 构造函数
    • 用于创建和初始化对象的特殊函数 => 后来演变为es6中的类
    // 定义一个构造函数
    function Person(name, age) {
        // 为新对象添加属性
        this.name = name;
        this.age = age;
    
        // 为新对象添加方法
        this.sayHi = function (value) {
            console.log(`hello ${this.name}`);
        };
    }
    
    // 使用 new 关键字调用构造函数创建对象
    const person = new Person("zhangsan", 20);
    person.sayHi(); // hello zhangsan
    console.log(person.constructor); // [Function: Person]
    
  • constructor
    • 每个对象都有一个constructor属性,指向创建该对象的构造函数,这个属性是从对象的原型链上继承而来的。
  • 存在的意义
    • 识别对象的类型 console.log(person.constructor === Person); // true
    • 构建新对象
    console.log(person.constructor === Person); // true
    const person2 = new person.constructor("lisi", 18);
    console.log(person2.sayHi());
    
    • 维护对象的继承关系

1.3 new一个对象是发生了什么?

function myNew(constructor, ...args) {
    // 1. 创建一个空对象 {}
    const newObj = {};

    // 2. 给该对象设置原型: 将新对象的原型指向构造函数的prototype属性
    newObj.__proto__ = constructor.prototype;

    // 3. 设置this,以新创建对象为this来执行构造函数,并传入参数
    const result = constructor.apply(newObj, args);

    // 4. 如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象
    return typeof result === "object" && result !== null ? result : newObj;
}

const person3 = myNew(Person, "xiaoming", 30);
console.log(person3); //  { name: 'xiaoming', age: 30, sayHi: [Function (anonymous)] }

2. 原型 & 原型链

2.1 对原型、原型链的理解?

  • 概念
    • 原型:每个对象都有一个内部属性 [[Prototype]],指向它的原型对象。原型对象也是一个对象,也有自己的原型,以此类推,直到最顶层的 Object.prototype
    • 原型链继承:当访问一个对象的属性或方法时,JavaScript 首先在对象本身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。
  • 如何获得对象非原型链上的属性?(即自身属性)
    • Object.getOwnPropertyNames() 方法
    • hasOwnProperty() 方法结合 for...in 循环

2.2 继承方式?

  • 原型链继承:让子类的原型指向父类的实例,这样子类实例就可以通过原型链访问到父类的属性和方法。
  • 构造函数继承:在子类构造函数中使用 callapply 或 bind 方法调用父类构造函数,将父类的属性和方法复制到子类实例中。
    // 父类构造函数
    function Parent(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    
    // 子类构造函数
    function Child(name) {
        Parent.call(this, name);
    }
    
    let child1 = new Child('child1');
    child1.colors.push('yellow');
    console.log(child1.colors); // ['red', 'blue', 'green', 'yellow']
    
    let child2 = new Child('child2');
    console.log(child2.colors); // ['red', 'blue', 'green']
    
  • es6类继承:引入了classextends关键字,更简洁,更符合面向对象

3. 变量提升 & 作用域

3.1 变量提升及作用域的理解

  • 现象
    • 变量声明提升
      • var声明的变量会被提升到当前作用域的顶部
      console.log(a); // undefined
      var a = 10;
      
      • let const暂时性锁区:使用 let 和 const 声明的变量也会提升,但它们存在 “暂时性死区”(TDZ)。在变量声明之前访问这些变量会导致 ReferenceError
    • 函数声明提升: 函数声明会被整体提升,意味着可以在函数声明之前调用该函数。
  • js实现原理
    • 编译阶段和执行阶段:
      • 编译阶段:在这个阶段,JavaScript 引擎会扫描代码,将变量和函数的声明存储在内存中。对于 var 声明的变量,会在当前作用域的变量对象(Variable Object)中创建一个属性,初始为 undefined;对于函数声明,会将整个函数定义存储在变量对象中。
      • 执行阶段:按照代码的顺序依次执行,当遇到变量赋值或函数调用时,会从变量对象中获取相应的值。
    • 作用域和作用域链:
      • 每个函数都有自己的作用域,作用域决定了变量和函数的可访问范围。当创建一个函数时,会创建一个新的作用域,并且该作用域会包含一个指向其父作用域的引用,这些作用域通过引用连接起来形成作用域链。
      • 在查找变量时,JavaScript 引擎会先在当前作用域的变量对象中查找,如果找不到,会沿着作用域链向上查找,直到找到该变量或到达全局作用域。
  • 变量提升存在的意义,导致了什么问题
    • 意义:1.提高代码的灵活性;2.符合编程习惯
    • 问题:1.代码可读性降低;2.意外的变量覆盖
  • 特殊case
    • 块级作用域中的var。由于var声明的变量没有块级作用域,i是在全局作用域共享的,当定时器执行时,循环已经结束,i的值已经变为5.
    console.log(i); // undefined
    for (var i = 0; i < 5; i++) {
        setTimeout(function () {
            console.log(i); // 输出: 5, 5, 5, 5, 5
        }, 100);
    }
    
    • 可以使用let来解决:let具有块级作用域,for循环的迭代中,let i每次会创建一块新的作用域,也就是说每次循环中的 i 都是一个独立的变量,它们拥有自己独立的存储空间。
    for (let i = 0; i < 5; i++) {
        setTimeout(function () {
            console.log(i); // 输出: 0,1,2,3,4
        }, 100);
    }
    

4. this 上下文 context

4.1 判断this指向

  • this概念及其作用:
    • 概念:js关键字,指向当前调用函数的对象,在不同的执行上下文中指向不同的对象,this是在函数调用时动态确认的,而不是在函数定义时确认的。
  • 判断this指向
    • 全局作用域:全局作用域中指向全局对象,浏览器环境中指向window image.png

    • 函数调用:

    // 非严格模式
    function test() {
        console.log(this); // 输出: Window 对象(在浏览器环境中)
    }
    test();
    
    // 严格模式
    function strictTest() {
        'use strict';
        console.log(this); // 输出: undefined
    }
    strictTest();
    
    • 方法调用: 指向调用该方法的对象
    const obj = {
        name: 'John',
        sayHello: function() {
            console.log(`Hello, my name is ${this.name}`);
        }
    };
    obj.sayHello(); // 输出: Hello, my name is John
    
    • 构造函数调用:指向新创建的对象
    • 箭头函数:没有自己的this,它的this继承自外层函数的this

4.2 如何改变this指向

  • callfunction.call(thisArg, arg1, arg2, ...)
  • applyfunction.apply(thisArg, [argsArray]),第二个参数必须以数组的形式传递
  • bindfunction.bind(thisArg, arg1, arg2, ...),不会立即调用函数,而是返回一个新函数,需要手动调用。

手写bind,可参考这篇文章:深入解析 bind 原理并自己实现 bind 和 apply

5. 闭包

  • 条件:
    • 1.函数嵌套函数;
    • 2.内层函数引用外层函数作用域中的变量或者参数;如果没有引用,那么就不存在闭包。
    • 3.外层函数需要被执行,并且返回内层函数。
    function outer() {
    const privateVariable = 1;
        function inner() {
            return privateVariable;
        }
        return inner();
    }
    const result = outer();
    console.log(result); // 1
    
  • 作用:
    • 读取函数内部的变量
    • 让这些变量始终保持在内存中
    • 封装私有变量和方法
  • 经典使用场景
    • 函数柯里化
      • 参数分步处理 :将 f(a, b, c) 转换为 f(a)(b)(c),每次调用只传递一个参数,逐步生成新函数
      • 闭包保存参数 :每个返回的新函数会记住之前传入的参数,直到所有参数收集完毕后执行最终计算
    const curriedAdd = function (a) {
        return function (b) {
            return a + b;
        };
    };
    // curriedAdd(3)的返回值是匿名函数,这个函数可以拿到curriedAdd中的参数a,再次调用的时候(5),相当于调用匿名函数,并且参数为5,然后做加法运算,返回结果8
    console.log(curriedAdd(3)(5)); // 输出: 8
    
    • 定时器,实现循环中的异步操作!!!:
      • 上面变量提升处讲到的for循环的题,也可以通过闭包的方式改造
      • 原理:通过立即执行函数,每次循环都会创建了一个新的闭包,每个闭包都会捕获当前i的值,并将其存储在index变量中,由于 index 是闭包内部的变量,每个定时器的回调函数都能正确访问到自己对应的 index 值,而不会受到循环变量 i 后续变化的影响。
    for (var i = 0; i < 5; i++) {
        (function (index) {
            setTimeout(function () {
                console.log(index); // 输出: 0,1,2,3,4
            }, 100);
        })(i);
    }
    
    • vuex中的使用
      • Vuex 的核心是管理应用的状态(state),并且让状态的变化能够自动更新到与之绑定的视图上,这依赖于响应式系统。而闭包在其中起到了维持对状态访问的作用。在 Vuex 中,模块的 mutations、actionsgetters 函数都可以形成闭包。例如,在一个模块的 getter 函数中:
      const store = new Vuex.Store({
          state: {
              count: 0,
          },
          getters: {
              // `doubleCount` 这个 getter 函数形成了闭包,它引用了外部的 `state` 对象。
              // 响应式系统会追踪 `state.count` 的变化,当 `state.count` 改变时,依赖于 `doubleCount` 的组件视图会自动更新。
              // 这里闭包确保了 getter 函数始终可以访问到最新的 `state`,为响应式系统提供了数据访问的基础。
              doubleCount: (state) => {
                  return state.count * 2;
              },
          },
      });
      

三、数组操作

1. 数组的基本操作方法有哪些?区别?

  • 增删改查
    • 增:push()尾增,unshift()首增,返回值:新数组长度,会改变原数组。
    • 删:pop()尾删, shift()首删,返回值:被删除的元素,改变原数组。
    • 改:直接通过索引修改
    • 查:indexOf():返回指定元素的第一个索引,不存在返回-1。includes() :是否包含某个元素,返回布尔值。其他:find(), findIndex()等。
  • 截取与拼接
    • 截取:slice(start, end):返回从 startend(不包括 end)的新数组,不改变原数组。
    • 插入:splice(start, deleteCount, ...items):从 start 开始删除 deleteCount 个元素,并插入 items,返回被删除的元素,改变原数组。
    • 拼接:concat():合并多个数组,返回新数组,原数组不变
  • 遍历与转换
    • 遍历:foreach(): 没有返回值,适合用于执行副作用操作(如修改外部变量、打印日志等), map(): 返回新数组,不改变原数组。
    • 过滤:filter():返回满足条件的元素组成的新数组,不改变原数组。
    • 计算:reduce():主要用于对数组中的每个元素执行一次提供的回调函数,并将其结果汇总为单个值。通常用于数组求和,扁平化嵌套数组等。
      reduce(callbackfn: (previousValue, currentValue, currentIndex, array));
      
      • 如何手动实现reduce()?
      // 挂载到Array的prototype上
      Array.prototype.myReduce = function (callback, initialValue) {
          // callback:回调函数,initialValue:初始值
          let array = this; // 数组即为调用函数的对象,即this
          let startIndex;
          let accumulator;
          if (initialValue === undefined) {
              if (!array.length) {
                  throw new TypeError("Reduce of empty array with no initial value");
              } else {
                  accumulator = array[0];
                  startIndex = 1;
              }
          } else {
              accumulator = initialValue;
              startIndex = 0;
          }
      
          // 循环调用callback函数
          for (let i = startIndex; i < array.length; i++) {
              accumulator = callback(accumulator, array[i]);
          }
      
          // 返回累计值
          return accumulator;
      };
      
      const arr = [1, 2, 3, 4, 5];
      const result = arr.myReduce((accumulator, current) => accumulator * current, 10);
      console.log("result: ", result); // result: 1200
      
    • 转换:join(): 将数组元素拼接成字符串。toString(), toLocalString()
  • 排序与反转
    • 排序:sort():返回排序结果(默认按字符串顺序),可传入比较函数,如:array.sort((a, b) => b - a)
    • 反转:reverse()

四、ES6

1. const对象的属性可以修改吗?

  • const只能保证变量引用的内存地址不变,而对象的属性可以修改的。

2. 箭头函数和普通函数有什么区别?

  • this指向
    • 普通函数:取决于函数的调用方式,它在不同的调用场景下指向不同的对象。常见的调用方式有全局调用、函数调用、方法调用、构造函数调用和 call/apply/bind 调用。
    • 箭头函数:this 指向定义时所在的对象,而不是调用时的对象,它继承自外层函数的 this 值,没有自己独立的 this
  • arguments对象
    • 普通:函数内部有一个 arguments 对象,它是一个类数组对象,包含了函数调用时传递的所有参数。
    • 箭头:没有自己的 arguments 对象,如果在箭头函数中使用 arguments,它会引用外层函数的 arguments 对象。如果是全局作用域的箭头函数,会报错!
      • 如何获取箭头函数的参数?剩余参数法
      const arrowFn = (...args) => {
          console.log(args);
      };
      arrowFn(1, 2, 3, 4, 4);
      
  • 使用new关键字
    • 普通:可以使用 new 关键字作为构造函数来创建对象实例。
    • 箭头:不能使用new调用,因为箭头函数没有prototype属性,也没有自己的this,使用new会抛出TypeError: arrowFn is not a constructor
  • 使用yield关键字
    • 普通:可以使用yield关键字将函数定义为生成器函数,用于实现异步编程和迭代器。
      function* test() {
          yield 1;
          yield 2;
      }
      const result = test();
      console.log(result.next()); // { value: 1, done: false }
      
    • 箭头:不能使用 yield 关键字,因此不能定义为生成器函数。

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

  • 全局对象:浏览器环境中全局对象是window、node环境中全局对象是global
  • 包装对象:String,Boolean
  • 日期对象:Date
  • 集合对象:Array,Map,Set
  • 错误对象:Error,TypeError
  • 正则表达式对象:RegExp

五、异步编程

1. 有哪些异步方式?

  • 回调函数 => cb 回调地狱
  • promise => 链式调用 => 语义不明确
  • generator => 考虑如何控制执行co库
  • async await => 不改变同步书写习惯的前提下,异步处理

2. 对Promise的理解

  • 一个对象、一个容器 => 出发操作
  • 三个状态:pending | resolved | rejected
  • 两个过程:pending => resolved | pending => rejected
  • 缺点:
    • 无法取消
    • 无细分状态

2.2 手写Promise

六、事件轮询(Event Loop):

blog.csdn.net/zkx529/arti…

  • 基本概念
    • js是单线程的,意味着同一时间只能执行一个任务,但实际开发中,通常有很多耗时操作(网络请求,文件读取等),如果同步执行就会引起页面卡顿等问题,所以js使用了异步编程机制,核心就是事件轮询
  • 工作原理:三部分组成(调用栈 + 任务队列 + 事件轮询器)
    • 任务队列:用于存储异步操作完成之后需要执行的回调函数。分为宏任务队列和微任务队列
      • 宏任务队列:script(整体代码)、setTimeoutsetIntervalsetImmediate(node环境)、I/O操作UI渲染(浏览器环境)
      • 微任务队列:promise.thenprocess.nextTick(node环境)、MutationObserver(浏览器环境)
    • !!!注意:new Promise是同步任务,promise.then才是异步任务的微任务。
  • 执行流程
    • 同步任务执行。压入调用栈
    • 异步任务处理。异步任务结束后,将回调函数放入相应的任务队列
    • 事件轮询。当调用栈为空时,事件轮询器开始工作。它会优先检查微任务队列,如果微任务队列中有任务,则依次将微任务从队列中取出并放入调用栈中执行,直到微任务队列为空。然后,事件轮询器会从宏任务队列中取出一个宏任务放入调用栈中执行,执行完毕后,再次检查微任务队列,重复上述过程。

七、TypeScript

什么是TS类型体操

  • 概念
    • 类型即代码。利用泛型、条件类型、映射类型等特性,像编写逻辑代码一样操作类型。

image.png

八、其他

8.1 eval是什么及其优缺点

  • eval(x:string)
    • 将传入的字符串作为 JavaScript 代码进行解析和执行。
    // eval是一个全局函数
    // 简单的算术表达式
    let expression = "2 + 3";
    let result = eval(expression);
    console.log(result); // 输出: 5
    
    // 执行代码块
    let code = "let x = 10; let y = 20; console.log(x + y);";
    eval(code); // 输出: 30
    
  • 优点:
    • 动态执行代码:例如,当你需要根据用户输入或外部数据动态生成代码并执行时,eval() 可以满足需求
    • 灵活性
    // 根据用户输入执行代码
    let userInput = prompt('请输入一个 JavaScript 表达式:');
    let userResult = eval(userInput);
    console.log('执行结果:', userResult);
    
  • 缺点
    • 安全风险:如XSS攻击
    • 性能问题:动态执行代码,所以效率会低
    • 调试困难:当代码出现问题时,很难定位到具体的错误位置,因为错误信息可能不够明确,无法直接指向原始代码中的问题。
    • 作用域问题:如果在 eval() 中定义了变量或函数,它们会成为当前作用域的一部分,可能会导致命名冲突和意外的行为。
  • 替代方案
    • 使用 JSON.parse () :当需要解析 JSON 字符串时,使用 JSON.parse() 而不是 eval(),因为 JSON.parse() 只能解析合法的 JSON 数据,更加安全。
    • 使用函数和数据结构:通过合理的函数设计和数据结构来实现动态逻辑,而不是依赖 eval() 执行动态代码。