ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## JavaScript有哪些数据类型,他们的区别是什么? * js中8中数据类型:Undefined、Null、Boolean、Number、String、Object(Array)、Symbol(ES6)、BigInt(ES6)。 * 原始数据类型(存于栈中):Undefined、Null、Boolean、Number、String,也称基本数据类型。 * 引用类型数据(存于堆中):Object(Array、Function)。 * null和undefined区别: undefined表示未定义,一般声明变量未赋值会返回undefined。 null代表的含义是空对象,主要用于赋值给一些可能返回对象的变量,作为初始化。 * ts类型:Boolean、Number、String、Array、Object、元数组、枚举、Any、Void、Null、Undefined、Never。(可能会问ts比es6中多哪些类型) **栈和堆的区别** * 存取方式:栈是先进后出,堆是一个优先队列,是按照优先级进行排序的,优先级可以按照大小来规定。 * 操作系统中:栈区是有编译器自动分配的,存放函数的参数值、局部变量等;堆区一般是由开发者分配释放的,若开发者不释放,程序结束时可能由垃圾回收机制回收(垃圾回收机制原理:通过计数为0或者标记进行回收)。 **后面两种是ES6新增的数据类型:** * Symbol代表创建后独一无二且不可变的数据类型,主要为了解决出现全局变量冲突问题。 * BigInt是一种数字类型的数据,他可以表示任意精度格式的整数,可以安全存储和操作大整数,可超出Number能够表示的安全整数范围。 ## 数据类型检测方式有哪些 * typeof:数组、对象、null都会判断为object,其他判断都正确。 * instanceof:可以判断对象类型(引用类型数据),内部原理是在其原型链中是否能找到该类型的原型。 * constructor:如 (2).constructor === Number // true,constructor有两个作用,意识判断数据类型,二是对象实例通过constructor对象访问它的构造函数,需要注意的是,这里用来创建一个对象来改变它的原型,就不能用来判断类型了。 * Object.prototype.toString.call():一般用于封装一个工具类函数,用于判断数据类型。 * 其他判断数组的方式:Array.isArray()、Array.prototype.isPrototypeOf(obj) ## ES6相关知识 **变量提升原因和存在问题** * 提升原因:提高性能(js代码执行之前,会进行语法检查和预编译,并且这操作只进行一次每次执行代码就不用再次解析一遍,因为变量和函数的代码不会随着执行而改变)和容错性更好(在一定程度上提供容错性,比如变量可以先试用后定义)。 * 存在问题,如下: ``` var tmp = 'vvmily'; function fn(){ console.log(tmp); if(false){ var tmp = 'hello world'; } } fn(); // undefined // 看到这里,在ES6中出现let和const,解决ES5之前的两个问题,具体请往下看。 ``` **let const var区别** * 块级作用域:块级作用域有{ }包括着,let和const具有块级作用域,var不存在块级作用域。 * 块级作用域解决ES5的两个问题: 1. 内层变量可能覆盖外层变量; 2. 用来计数的循环变量泄露为全局变量。 * var:存在变量提升;定义变量为全局变量;可以重复声明变量; * let/const:不存在变量提升,变量只能先定义在使用,否则报错;不会给全局定义变量;let和const都是ES6新增的用于创建变量的语法,let创建的变量是可以更改指针指向(可以重新赋值);但const声明的变量是不允许改变指针的指向。 ``` const Len = 3; // Len已经不可以改变 const Obj = { name: 'vvmily' }; // Obj.name是可以改变的 ``` * const:const定义变量必须设置初始值。 **箭头函数相比于普通函数** * 箭头函数:没有自己的this,继承上一层作用域的this,所以箭头函数中的this的指向在它定义时已经确定了,之后不会再改变。 * 不能作为构造函数使用、也没有prototype、没有argument、不能作用Generator函数不能使用yeild关键字。 * apply、bind和call都不能改变箭头函数中的this。 **扩展运算符(...)作用和使用场景** * 对 对象/数组 解构赋值。 * 将字符串转化为真正的数组:`[...'hello'] // [ "h", "e", "l", "l", "o" ]`。 * 将Iterator接口对象等,如arguments转真正的对象`const args = [...arguments]`。 * 其他:`const numbers = [2,1,5,3]; Math.min(...numbers); // 1`。 **map和weakMap的区别** * **map**:本质上就是键值对的集合,但是普通的Object的键只能是字符串,但是map的键不限制范围,可以任意类型。 * Map数据结构有以下操作方法: 1. **size**: `map.size` 返回Map结构的成员总数。 2. **set(key,value)**:设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用) 3. **get(key)**:该方法读取key对应的键值,如果找不到key,返回undefined。 4. **has(key)**:该方法返回一个布尔值,表示某个键是否在当前Map对象中。 5. **delete(key)**:该方法删除某个键,返回true,如果删除失败,返回false。 6. **clear()**:map.clear()清除所有成员,没有返回值。 * Map结构原生提供是三个遍历器生成函数和一个遍历方法: 1. keys():返回键名的遍历器。 2. values():返回键值的遍历器。 3. entries():返回所有成员的遍历器。 4. forEach():遍历Map的所有成员。 * **weakMap**:对象也是一组键值对集合,其中的键是弱引用的,键必须是对象(null除外),基础数据(String)不能作为键。 * 该对象也有以下几种方法: 1. **set(key,value)**:设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用) 2. **get(key)**:该方法读取key对应的键值,如果找不到key,返回undefined。 3. **has(key)**:该方法返回一个布尔值,表示某个键是否在当前Map对象中。 4. **delete(key)**:该方法删除某个键,返回true,如果删除失败,返回false。 **ES6模块ES6Module与CommonJS模块有什么区别** 相同点:都可以对引入的对象进行赋值(对 对象内部属性的值进行改变)。 不同点:CommonJS对模块的浅拷贝,可以对CommonJS对象重新赋值(可以修改指针)。 ES6Module对模块的引用(只存只读),不能改变其值,或者说指针不能变,类似const定义引用类型。 ## JavaScript基础 **new操作符的原理** * 创建的过程: 1. 创建一个新的对象; 2. 设置原型,将函数的prototype对象赋给这个新对象的原型; 3. 让函数的this指向这个新对象,执行构造函数代码(给新对象添加属性); 4. 判断函数返回值的类型,如果是值类型,则返回创建的新对象,如果是引用类型则返回这个引用类型的对象。 * 大致实现代码: ``` js function MyNew(){ var obj = {}, // 截取类数组arguments第一个参数并返回,其实就是构造函数 // 注意:shift会改变原数组,故改变指向使用apply对应第二个参数为去除构造函数剩余的数组arguments Constructor = [].shift.call(arguments) // 构造函数原型赋值给obj对象 obj.__proto__ = Constructor.prototype // 改变构造函数指向,指向obj对象 Constructor.apply(obj, arguments) // 第三步,这里返回值可以根据第四步说明判断相继完善即可 return obj } MyNew(fn,params) //使用方法 ``` **JavaScript类数组对象** * 一个拥有length属性和索引属性的对象可以被称之为类数组对象(函数也算一个,因为函数也有length属性值),但是不能调用数组的方法。 * 类数组转真正的数组方法: 1. Array.prototype.slice.call(arrayLike) // arrayLike类数组 2. Array.prototype.splice.call(arrayLike,0) 3. Array.prototype.cancat.apply([],arrayLike) 4. Array.from(arrayLike) 5. [...arrayLike] **escape、encodeURI、encodeURIComponent 的区别** * encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。 * encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。 * escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。 **对AJAX的理解,实现一个AJAX请求** * AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。 * 创建AJAX请求步骤: 1. 创建一个XMLHttpRequest对象 2. 在这个对象上使用open方法创建一个http请求,open方法参数是 请求的方法、请求的地址、是否异步和用户认证的信息。 3. 发起请求前,可以为这个对象添加一些信息和监听函数,比如通过setRequestHeader为请求头设置信息,通过这个对象的onreadystatechange的readyState变为4,表示服务器返回数据接收完成,状态为200/304等代表正常。 4. 当对象属性和监听方法设置完成后,最后调用send方法向服务器发起请求,可以传入参数作为发送数据体。 ``` const SERVER_URL = "/base"; let xhr = new XMLHttpRequest(); // 创建 Http 请求 xhr.open("GET", url, true); // 设置状态监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功时 if (this.status === 200) { handle(this.response); } else { console.error(this.statusText); } }; // 设置请求失败时的监听函数 xhr.onerror = function() { console.error(this.statusText); }; // 设置请求头信息 xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); // 发送 Http 请求 xhr.send(null); ``` **for...in和for...of区别** * for...of是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等),并且返回的各项的值。 * 两者区别: 1. for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名; 2. for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链; 3. 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值; * 总结:for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。 **ajax、axios、fetch的区别** * ajax即AsynchronousJavascriptAndXML(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。它是一种无需在加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量的数据交换,Ajax实现网页异步更新。 * fetch在ES6出现,号称是Ajax替代品,使用了ES6中promise对象,或者说基于promise设计的。fetch不是Ajax的进一步封装,而是原生JavaScript,没有使用XMLHttpRequest对象。 * fetch优点: 1. 语法简洁,更加语义化 2. 基于标准的promise对象实现支持async/await 3. 更加底层,提供的API更丰富(request、response) 4. 脱离了XML,是ES规范里新的实现方式 * fetch缺点: 1. 只针对网络请求报错,对400、500并不会reject,而是当作成功请求,反之当网络错误不能完成请求,才会被reject。 2. 默认不会带Cooke,需要添加配置项: fetch(url, {credentials: 'include'}) 3. 不支持abort,不支持超时监控,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费 4. fetch没有办法原生监测请求的进度,而XHR可以 * axios:是一种基于Promise封装的HTTP客户端,其特点如下: 1. 浏览器端发起XMLHttpRequest请求、node端发起HTTP请求 2. 支持Promise API 3. 监听请求和返回,对请求和返回进行转化 4. 取消请求 5. 自动转换json数据 6. 客户端支持抵御XSRF攻击 **forEach和map方法有什么区别** 这方法都是用来遍历数组的,两者区别如下: * forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值。 * map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值。 ## 原型与原型链 **对原型和原型链理解** * 在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 __proto__ 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。 * 原型链:当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。 * 特点:JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。 ![](https://img.kancloud.cn/ee/40/ee40304a3ee611a22dfb3593de1b6a97_618x781.png) **原型修改、重写** ``` 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 ``` **原型链指向** ``` 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 ``` **原型链的终点是什么?如何打印出原型链的终点?** 由于`Object`是构造函数,原型链终点是`Object.prototype.__proto__`,而`Object.prototype.__proto__=== null // true`,所以,原型链的终点是`null`。原型链上的所有原型都是对象,所有的对象最终都是由`Object`构造的,而`Object.prototype`的下一级是`Object.prototype.__proto__`。 ![](https://img.kancloud.cn/3e/81/3e81af5d27841525cf5b08f521a4d1b2_490x146.png) **如何获得对象非原型链上的属性?** 使用后`hasOwnProperty()`方法来判断属性是否属于原型链的属性: ``` function iterate(obj){ var res=[]; for(var key in obj){ if(obj.hasOwnProperty(key)) res.push(key+': '+obj[key]); } return res; } ``` ## 闭包的理解 **闭包是指有权访问另一个函数作用域中变量的函数**,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。 * 闭包有两个常用的用途: 1. 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。 2. 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。 * 比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。 ``` function A() { let a = 1 window.B = function () { console.log(a) } } A() B() // 1 ``` * 经典面试题:循环中使用闭包解决 var 定义函数的问题,在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。 ``` for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) // 最后输出,都是6,解决方式有多种,往下看 }, i * 1000) } ``` 首先因为 `setTimeout` 是个异步函数,所以会先把循环全部执行完毕,这时候 `i` 就是 6 了,所以会输出一堆 6。解决方式提供下面三种: 1. 方式一(使用闭包的方式): 首先使用了立即执行函数将 `i` 传入函数内部,这个时候值就被固定在了参数 `j` 上面不会改变,当下次执行 `timer` 这个闭包的时候,就可以使用外部函数的变量 `j`,从而达到目的。 ``` for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) } ``` 2. 方式二(就是使用 `setTimeout` 的第三个参数,这个参数会被当成 `timer` 函数的参数传入。): ``` for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i ) } ``` 3. 方式三(就是使用 `let` 定义 `i` 了来解决问题了,这个也是最为推荐的方式): ``` for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) } ``` ## 作用域、作用域链的理解 **全局作用域和函数作用域** * 全局作用域 1. 最外层函数和最外层函数外面定义的变量拥有全局作用域 2. 所有未定义直接赋值的变量自动声明为全局作用域 3. 所有window对象的属性拥有全局作用域 4. 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。 * 函数作用域 1. 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到 2. 作用域是分层的,内层作用域可以访问外层作用域,反之不行 **块级作用域** * 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由`{ }`包裹的代码片段) * let和const声明的变量不会有变量提升,也不可以重复声明 * 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。 **作用域链:** * 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。 * 作用域链的作用是**保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。** * 作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。 * 当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。 ## call/apply/bind **call() 和 apply() 的区别?** 它们的作用一模一样,区别仅在于传入参数的形式的不同。 * apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。 * call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。 **实现call、apply 及 bind 函数** * call 函数的实现步骤: 1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 3. 处理传入的参数,截取第一个参数后的所有参数。 4. 将函数作为上下文对象的一个属性。 5. 使用上下文对象来调用这个方法,并保存返回结果。 6. 删除刚才新增的属性。 7. 返回结果。 ``` 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; }; ``` * apply 函数的实现步骤: 1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 3. 将函数作为上下文对象的一个属性。 4. 判断参数值是否传入 5. 使用上下文对象来调用这个方法,并保存返回结果。 6. 删除刚才新增的属性 7. 返回结果 ``` 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; }; ``` * bind 函数的实现步骤: 1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。* 保存当前函数的引用,获取其余传入参数值。 2. 创建一个函数返回 3. 函数内部使用 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) ); }; }; ``` ## 实现异步编程的方式 * **回调函数** 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。 * **Promise** 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。 * **generator** 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。 * **async 函数** 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。 **对Promise的理解** * Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。 * 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 1. Promise的实例有**三个状态**: * Pending(进行中) * Resolved(已完成) * Rejected(已拒绝) * 当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。 2. Promise的实例有**两个过程**: * pending -> fulfilled : Resolved(已完成) * pending -> rejected:Rejected(已拒绝) * 注意:一旦从进行状态变成为其他状态就永远不能更改状态了。 * Promise的缺点: 1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。 2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。 3. 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 * 总结: 1. Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。 2. 状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。 * 注意:在构造 `Promise` 的时候,构造函数内部的代码是立即执行的。 ## 垃圾回收机制与内存泄漏 **浏览器的垃圾回收机制** * 垃圾回收的概念 **垃圾回收**:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。 * **回收机制**: 1. Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。 2. JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。 3. 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。 * 垃圾回收方式:浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。 * 标记清除 1. 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。 2. 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。 * 引用计数 1. 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。 2. 这种方法会引起**循环引用**的问题:例如: `obj1`和`obj2`通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,`obj1`和`obj2`还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。 **哪些情况会导致内存泄漏** 1. **意外的全局变量:**由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。 2. **被遗忘的计时器或回调函数:**设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 3. **脱离 DOM 的引用:**获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。 4. **闭包:**不合理的使用闭包,从而导致某些变量一直被留在内存当中。