知识点概览
一、数据类型 & 类型检测&类型转换
1. 数据类型种类
- 8种数据类型:
Number,String,Boolean,Null, Undefined,Object, Symbol,BigInt - 新增的
Symbol和BigInt:Symbol:表示独一无二的值,主要用于对象属性名,避免属性名冲突。每个Symbol都是唯一的,可作为对象私有属性,不被一些遍历方法访问。
const symb = Symbol("symb"); const obj = { [symb]: "test hahah", }; console.log(obj[symb]); // test hahahBigInt:表示任意精度的整数,安全的存储和操作大数据,即便超出了Number安全整数范围- 使用场景:金融领域的精度计算,大数据处理,密码学,游戏开发中的资源计数(金币,经验值等)
// 写法1:使用BigInt const bigData = BigInt(9007199254740991); // 写法2:使用n后缀 const bigData2 = 2345678902345456782345n; null和undefined有什么区别?- 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 0:void是一个元运算符,可以对任何表达式操作,然后返回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需要多一步调用操作,繁琐,不适合
- 改变this指向:
- 为什么
obj.toString()和Object.prototype.toString.call(obj)结果可能不同?obj.toString()调用的是对象自身或其原型链上的toString方法,这个方法可能已经被重写或有特定的功能,而Object.prototype.toString.call(obj)强制使用Object的toString方法并将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 isNaN和Number.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 => NaNNull => 0Boolean => true: 1 | false: 0String => 包含非数字的值:NaN | 空: 0Symbol => 报错对象 => 相应的基本值类型 => 相应的转换
- 转换为字符串:
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
- js对象的本质并不是直接基于类,而是基于 构造函数+原型链传递方式 =>
- 优势
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 继承方式?
- 原型链继承:让子类的原型指向父类的实例,这样子类实例就可以通过原型链访问到父类的属性和方法。
- 构造函数继承:在子类构造函数中使用
call、apply或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类继承:引入了
class和extends关键字,更简洁,更符合面向对象
3. 变量提升 & 作用域
3.1 变量提升及作用域的理解
- 现象
- 变量声明提升:
var声明的变量会被提升到当前作用域的顶部
console.log(a); // undefined var a = 10;let和const暂时性锁区:使用let和const声明的变量也会提升,但它们存在 “暂时性死区”(TDZ)。在变量声明之前访问这些变量会导致ReferenceError。
- 函数声明提升: 函数声明会被整体提升,意味着可以在函数声明之前调用该函数。
- 变量声明提升:
- js实现原理
- 编译阶段和执行阶段:
- 编译阶段:在这个阶段,JavaScript 引擎会扫描代码,将变量和函数的声明存储在内存中。对于
var声明的变量,会在当前作用域的变量对象(Variable Object)中创建一个属性,初始为undefined;对于函数声明,会将整个函数定义存储在变量对象中。 - 执行阶段:按照代码的顺序依次执行,当遇到变量赋值或函数调用时,会从变量对象中获取相应的值。
- 编译阶段:在这个阶段,JavaScript 引擎会扫描代码,将变量和函数的声明存储在内存中。对于
- 作用域和作用域链:
- 每个函数都有自己的作用域,作用域决定了变量和函数的可访问范围。当创建一个函数时,会创建一个新的作用域,并且该作用域会包含一个指向其父作用域的引用,这些作用域通过引用连接起来形成作用域链。
- 在查找变量时,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是在函数调用时动态确认的,而不是在函数定义时确认的。
- 概念:js关键字,指向当前调用函数的对象,在不同的执行上下文中指向不同的对象,
- 判断this指向
-
全局作用域:全局作用域中指向全局对象,浏览器环境中指向window
-
函数调用:
// 非严格模式 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指向
call:function.call(thisArg, arg1, arg2, ...)apply:function.apply(thisArg, [argsArray]),第二个参数必须以数组的形式传递bind:function.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、actions和getters函数都可以形成闭包。例如,在一个模块的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):返回从start到end(不包括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):
- 基本概念
- js是单线程的,意味着同一时间只能执行一个任务,但实际开发中,通常有很多耗时操作(网络请求,文件读取等),如果同步执行就会引起页面卡顿等问题,所以js使用了异步编程机制,核心就是事件轮询。
- 工作原理:三部分组成(调用栈 + 任务队列 + 事件轮询器)
- 任务队列:用于存储异步操作完成之后需要执行的回调函数。分为宏任务队列和微任务队列
- 宏任务队列:
script(整体代码)、setTimeout、setInterval、setImmediate(node环境)、I/O操作、UI渲染(浏览器环境) - 微任务队列:
promise.then、process.nextTick(node环境)、MutationObserver(浏览器环境)
- 宏任务队列:
- !!!注意:
new Promise是同步任务,promise.then才是异步任务的微任务。
- 任务队列:用于存储异步操作完成之后需要执行的回调函数。分为宏任务队列和微任务队列
- 执行流程
- 同步任务执行。压入调用栈
- 异步任务处理。异步任务结束后,将回调函数放入相应的任务队列
- 事件轮询。当调用栈为空时,事件轮询器开始工作。它会优先检查微任务队列,如果微任务队列中有任务,则依次将微任务从队列中取出并放入调用栈中执行,直到微任务队列为空。然后,事件轮询器会从宏任务队列中取出一个宏任务放入调用栈中执行,执行完毕后,再次检查微任务队列,重复上述过程。
七、TypeScript
什么是TS类型体操
- 概念
- 类型即代码。利用泛型、条件类型、映射类型等特性,像编写逻辑代码一样操作类型。
八、其他
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()执行动态代码。
- 使用 JSON.parse () :当需要解析 JSON 字符串时,使用