目录
六、 Javascript 为什么要进行变量提升,它导致了什么问题?
十四、Object.is() 与比较操作符 === 、== 的区别
二十二、async / await 对比 Promise 的优势
new操作符的实现原理
具体实现
- function objectFactory() {
- let newObject = null;
- let constructor = Array.prototype.shift.call(arguments);
- let result = null;
- // 判断参数是否是一个函数
- if (typeof constructor !== "function") {
- console.error("type error");
- return;
- }
- // 新建一个空对象,对象的原型为构造函数的 prototype 对象
- newObject = Object.create(constructor.prototype);
- // 将 this 指向新建对象,并执行函数
- result = constructor.apply(newObject, arguments);
- // 判断返回对象
- let flag = result && (typeof result === "object" || typeof result === "function");
- // 判断返回结果
- return flag ? result : newObject;
- }
- function person(name, age) {
- this.name = name
- this.age = age
- }
- const value = objectFactory(person, '我是', '地瓜')
- console.log(value) // person {name: '我是', age: '地瓜'}
- console.log(value.__proto__ === person.prototype); // true
一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有arguments和DOM方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有length属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:
Array.prototype.slice.call(array);
Array.prototype.splice.call(array, 0);
Array.prototype.concat.apply([], array);
Array.from(array);
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:
- const SERVER_URL = "/server";
- 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);
使用Promise封装AJAX:
- function getJSON(url) {
- // 创建一个 promise 对象
- let promise = new Promise(function(resolve, reject) {
- let xhr = new XMLHttpRequest();
- // 新建一个 http 请求
- xhr.open("GET", url, true);
- // 设置状态的监听函数
- xhr.onreadystatechange = function() {
- if (this.readyState !== 4) return;
- // 当请求成功或失败时,改变 promise 的状态
- if (this.status === 200) {
- resolve(this.response);
- } else {
- reject(new Error(this.statusText));
- }
- };
- // 设置错误监听函数
- xhr.onerror = function() {
- reject(new Error(this.statusText));
- };
- // 设置响应的数据类型
- xhr.responseType = "json";
- // 设置请求头信息
- xhr.setRequestHeader("Accept", "application/json");
- // 发送 http 请求
- xhr.send(null);
- });
- return promise;
- }
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
解析阶段:JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
执行阶段:就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
- a = 1;
- var a;
- console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:
- var tmp = new Date();
-
- function fn(){
- console.log(tmp);
- if(false){
- var tmp = 'hello world';
- }
- }
-
- fn(); // undefined
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
- var tmp = 'hello world';
-
- for (var i = 0; i < tmp.length; i++) {
- console.log(tmp[i]);
- }
-
- console.log(i); // 11
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。
JavaScript 共有八种数据类型,分别是Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt
其中 Symbol 和 BigInt 是ES6中新增的数据类型
这些数据可以分为原始数据类型和引用类型:
两种类型的区别在于存储位置的不同:
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
在操作系统中,内存被分为栈区和堆区:
- 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,其他类型判断都正确
instanceof 可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型,instanceof 只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
- 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
constructor 有两个作用:1.判断数据的类型 2.对象实例通过 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 就不能用来判断数据类型
- function Fn() {};
- Fn.prototype = new Array();
- var f = new Fn();
- console.log(f.constructor === Fn); // false
- console.log(f.constructor === Array); // true
Object.prototype.toString.call() 使用 Object 对象的原型方法toString 来判断数据类型
- var a = Object.prototype.toString;
- console.log(a.call(2)); // [object Number]
- console.log(a.call(true)); // [object Boolean]
- console.log(a.call('str')); // [object String]
- console.log(a.call([])); // [object Array]
- console.log(a.call(function(){})); // [object Function]
- console.log(a.call({})); // [object Object]
- console.log(a.call(undefined)); // [object Undefined]
- console.log(a.call(null)); // [object 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(obj).slice(8,-1) === 'Array';
obj.__proto__ === Array.prototype;
Array.isArray(obj);
obj instanceof Array
Array.prototype.isPrototypeOf(obj);
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当对这两种类型使用 typeof 进行判断时,Null 类型会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
- function myInstanceof(left, right) {
- // 获取对象的原型
- let proto = Object.getPrototypeOf(left);
- // 获取构造函数的 prototype 对象
- let prototype = right.prototype;
- // 判断构造函数的 prototype 对象是否在对象的原型链上
- while(true) {
- if (!proto) return false;
- // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
- if (proto === prototype) return true;
- }
- }
toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。
- let n1 = 0.1, n2 = 0.2;
- console.log(n1 + n2); // 0.30000000000000004
- console.log((n1 + n2).toFixed(2)); // 0.30
注意:toFixed为四舍五入
对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:

(1)块级作用域
块作用域由{ }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
(2)变量提升
var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性
浏览器的全局对象是window,Node.js的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明
var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区
在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置
在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向
let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | × | √ | √ |
| 是否存在变量提升 | √ | × | × |
| 是否添加全局属性 | √ | × | × |
| 能否重复声明变量 | √ | × | × |
| 是否存在暂时性死区 | × | √ | √ |
| 是否必须设置初始值 | × | × | √ |
| 能否改变指针指向 | √ | √ | × |
(1)箭头函数比普通函数更加简洁
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值。
(7)箭头函数没有prototype
(8)箭头函数不能用作Generator函数,不能使用yeild关键字
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 __proto__ 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
特点:JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

构造函数的原型的__proto__指向的是Object的原型,Object的原型constructor指向的是自己,Object的原型的__proto__指向的是null。
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
(1)Promise的实例有三个状态:
当把一件事情交给Promise时,它的状态就是Pending,任务完成了状态就变成了Resolved,没有完成失败了就变成了Rejected。
(2)Promise的实例有两个过程:
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
Promise的特点:
Promise的缺点:
总结:
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的
(1)创建Promise对象
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。
- const promise = new Promise(function(resolve, reject) {
- // ... some code
- if (/* 异步操作成功 */){
- resolve(value);
- } else {
- reject(error);
- }
- });
一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject这两个方法:
Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:
- Promise.resolve(16).then(function(value){
- console.log(value); // 打印出16
- });
resolve(16) 代码中,会让promise对象进入确定(resolve状态),并将参数16传递给后面的then所指定的onFulfilled函数。
创建promise对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建promise对象。
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("Failed");
- }
- });
- };
-
- // 方法调用
- 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的第二个函数内,会打印Failed。
(2)Promise方法
Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。
当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方法来解决。
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);
-
- });
all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。
- 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执行时的值。
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=>{})
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise.then(result => {···}).catch(error => {···}).finally(() => {···});
上面代码中,不管`promise`最后的状态,在执行完then或catch指定的回调函数以后,都会执行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方法,则只需要写一次。
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)

所以,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 实例。
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途:
比如,函数 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)
- }
(1)全局作用域
(2)函数作用域
(3)块级作用域
作用域链
在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
1. 执行上下文类型
(1)全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
(2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
(3)eval() 函数执行上下文
执行在eval() 函数中的代码会有属于他自己的执行上下文,不过eval() 函数不常使用,不做介绍。
2. 执行上下文栈
- 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)this绑定
(2)创建词法环境组件
(3)创建变量环境组件
变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
执行阶段
此阶段会完成对变量的分配,最后执行完代码。
简单来说执行上下文就是指:
在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
它们的作用一模一样,区别仅在于传入参数的形式的不同。
(1)call 函数的实现步骤:
- 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 函数的实现步骤:
- 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 函数的实现步骤:
- 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)
- );
- };
- };
一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:
(1)垃圾回收的概念
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
(2)垃圾回收的方式
浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。
(1)标记清除
(2)引用计数
- function fun() {
- let obj1 = {};
- let obj2 = {};
- obj1.a = obj2; // obj1 引用 obj2
- obj2.a = obj1; // obj2 引用 obj1
- }
这种情况下,就要手动释放变量占用的内存:
- obj1.a = null
- obj2.a = null
(3)减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。