# JavaScript 有哪些数据类型,有什么区别
JavaScript 共 8 种数据类型:
- Undefined
- Null
- Boolean
- Number
- String
- Object
- Symbol
- 创建后独一无二且不可变的数据类型
- 可用于解决可能出现的全局变量冲突
- BigInt
- 数字类型,可以表示任意精度格式的整数
- 使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 的范围
以上数据类型可以分为原始数据类型 和 引用数据类型:
- 栈:原始数据类型 (Undefined, Null, Boolean, Number, String, Symbol, BigInt)
- 对象:引用数据类型 (对象、数组、函数)
以上两种类型的区别在于存储位置的不同:
- 原始数据类型直接存储在栈(stack)中的简单数据段
- 占据空间小、大小固定
- 属于被频繁使用的数据,所以放入栈中存储
- 引用数据类型存储在堆(heap)中的对象
- 占据空间大、大小不固定
- 如果存储在栈中,会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的妻子地址。当解释器寻找引用值时,会首先检索其栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中:
- 在数据结构中:
- 在数据结构中,栈中数据的存取方式为先进后出;
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定;
- 在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
- 堆区内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收
# 数据类型检查方法有哪些
# typeof
代码语言:javascript复制typeof 2; // number
typeof true; // boolean
typeof "s"; // string
typeof []; // object
typeof function() {}; // function
typeof {}; // object
typeof undefined; // undefined
typeof null; // object
注意:数组、对象、null 都会被判断为 object,其他判断都正确。
# instanceof
instanceof
可以正确判断对象的类型,其内部运行机制是判断其原型链中能否找到该类型的原型。
2 instanceof Number; // false
true instanceof Boolean; // false
"s" instanceof String; // false
[] instanceof Array; // true
function() {} instanceof Function; // true
{} instanceof Object; // true
注意:instanceof
只能正确判断引用数据类型,不能判断基本数据类型。instanceof
可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype
属性。
# constructor
代码语言:javascript复制(2).constructor === Number; // true
(true).constructor === Boolean; // true
("s").constructor === String; // true
([]).constructor === Array; // true
(function() {}).constructor === Function; // true
({}).constructor === Object; // true
constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constructor
对象访问它的构造函数。注意,如果创建一个对象来改变它的原型,constructor
就不能用了判断数据类型了。
function Fn() {}
Fn.prototype = new Array();
var f = new Fn();
f.constructor === Fn; // false
f.constructor === Array; // true
# Object.prototype.toString.call()
代码语言:javascript复制var obj2Str = Object.prototype.toString;
obj2Str.call(2); // "[object Number]"
obj2Str.call(true); // "[object Boolean]"
obj2Str.call("s"); // "[object String]"
obj2Str.call([]); // "[object Array]"
obj2Str.call(function() {}); // "[object Function]"
obj2Str.call({}); // "[object Object]"
obj2Str.call(undefined); // "[object Undefined]"
obj2Str.call(null); // "[object Null]"
注意: obj.toString()
和 Object.prototype.toString.call(obj)
结果不一样:
- 因为
toString
是Object
的原型方法,而Array
、Function
等类型作为Object
的实例,都重写了toString
方法。 - 不同的对象类型调用
toString
方法时,根据原型链的知识,调用的是对应的重写之后的toString
方法,而不是去调用Object
上原型toString
方法,所以obj.toString()
不能得到其对象类型,只能将obj
转换为字符串类型。
# null 和 undefined 的区别
Undefined
和 Null
都是基本数据类型,分别只有一个值:undefined
,null
。
undefined
代表 未定义,一般变量声明了但还没有定义的时候会返回undefined
null
代表 空对象,null
主要用于赋值给一些可能会返回对象的变量,做初始化
undefined
在 JavaScript 中不是一个保留字,即可以使用undefined
作为一个变量名,但这样很危险,会影响对undefined
值的判断。可以通过一些方法获得安全的undefined
值,如void 0
。
在使用 typeof
进行判断时,Null
类型会返回 object
,这是一个历史遗留问题。
null == undefined; // true
null === undefined; // false
# instanceof 原理及实现
instanceof
用于判断构造函数的 prototype
属性是否出现在对象的原型链中的任何位置。
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left);
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 检查构造函数的 `prototype` 对象是否在对象的原型链中
while (true) {
// 如果已经到达原型链的最顶端 null,则返回 false
if (!proto) return false;
if (proto === prototype) return true;
// 沿着原型链向上查找
proto = Object.getPrototypeOf(proto);
}
}
# 获取安全的 undefined 值
因为 undefined
是一个标识符,所以可以被当做变量来使用和赋值,这会影响 undefined
的判断。表达式 void
没有返回值,因此返回结果是 undefined
。 void
并不改变表达式的结果,只是让表达式返回值。因此可以用 void 0
来获得 undefined
。
# Object.is()
与 ===
、==
的区别
- 使用
==
进行判断时,如果两边类型不一致,会进行强制类型转换 - 使用
===
进行判断时,如果两边类型不一致,不会做强制类型转换,直接返回false
- 使用
Object.is()
进行判断时,一般情况下和===
相同,不过处理了一些特殊情况,如-0
和0
不再相等,两个NaN
是相等的
# 什么是 JavaScript 中的包装类型
在 JavaScript 中,基本类型是没有属性和方法的,但为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型转换为对象。
代码语言:javascript复制const s = 'cellinlab'; // 后台转换成 String('cellinlab')
s.length; // 7
s.toUpperCase(); // 'CELLINLAB'
JavaScript 也能使用 Object 函数显示地将基本类型转换为包装类型:
代码语言:javascript复制var s = 'cellinlab';
Object(s); // String {'cellinlab'}
也可以使用 valueOf
方法将包装类型倒转成基本类型:
var s = 'cellinlab';
var s1 = Object(s);
var s2 = s1.valueOf(); // 'cellinlab'
注意:
false
包装成类型后变为对象,其非值为false
# 为什么会有 BigInt
JavaScript 中 Number.MAX_SAFE_INTEGER
表示最大安全数字,计算结果是 9007199254740991
,即在这个数范围内不会出现精度丢失(小数除外)。一旦超过这个范围,JavaScript 就会出现计算不准确的情况,在大数计算时不得不依靠一些三方库来解决,因此官方提出了 BigInt 来解决这个问题。
# 如何判断一个对象是空对象
使用 JSON.stringify()
判断
JSON.stringify({}) === '{}';
使用 Object.keys()
判断
Object.keys({}).length === 0;
# const 对象的属性可以修改吗
const
保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型数据(对象、数组),变量指向数据的内存地址,保存的只是一个指针,const
只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
# new 一个箭头函数会发生什么
箭头函数是 ES6 中提出来的,它没有 prototype
,也没有自己的 this
指向,更不可以使用 arguments
参数,所以不能 new
一个箭头函数。
new
操作符的实现步骤如下:
- 创建一个对象
- 将构造函数的作用域赋给新对象(即将对象的
__proto__
指向构造函数的prototype
) - 执行构造函数中的代码,构造函数中的
this
指向该对象(即为这个对象添加属性和方法) - 返回新对象
# 箭头函数的 this 指向哪里
箭头函数不同于传统 JavaScript 中的函数,箭头函数并没有属于自己的 this
,它所谓的 this
是捕获其所在上下文的 this
值,作为自己的 this
值。并且由于没有属于自己的 this
,所以不会被 new
调用。
可以用 Babel 理解一下箭头函数:
代码语言:javascript复制// ES6
const obj = {
getArrow () {
return () => {
console.log(this === obj);
}
}
}
转化后:
代码语言:javascript复制// ES5
var obj = {
getArrow: function() {
var _this = this;
return function() {
console.log(_this === obj);
}
}
}
# 扩展运算符作用及其使用场景
# 对象扩展运算符
对象扩展运算符...
用于取出参数对象中所有可遍历属性,拷贝到当前对象中。
let bar = { a: 1, b: 2 };
let baz = {...bar}; // { a: 1, b: 2 }
相当于:
代码语言:javascript复制let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }
Object.assign()
用于对象的合并,将源对象的所有可枚举属性,复制到目标对象:
- 第一个参数是目标对象,后面的参数都是源对象
- 如果目标对象与源对象有同名属性,或多个源对象有同名属性,这后面的属性会覆盖前面的属性
同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
代码语言:javascript复制let bar = { a: 1, b: 2 };
let baz = {...bar, ...{a: 2, b: 4}}; // { a: 2, b: 4 }
利用上述特性可以很方便地修改对象的部分属性。
注意:扩展运算符对对象的实例的拷贝属于浅拷贝。
# 数组扩展运算符
数据扩展运算符可以将一个数组转为逗号分隔的参数序列,且每次只能展开一层数组。
代码语言:javascript复制console.log(...[1, 2, 3]); // 1 2 3
console.log(...[1, [2, 3], 4]); // 1 [2, 3] 4
应用:
将数组转换为参数序列
代码语言:javascript复制function add (x, y) {
return x y;
}
const number = [1, 2];
console.log(add(...number)); // 3
复制数组
代码语言:javascript复制const arr = [1, 2, 3];
const arr2 = [...arr]; // [1, 2, 3]
合并数组
代码语言:javascript复制const arr1 = [1, 2, 3];
const arr2 = [4, ...arr1, 5, 6]; // [4, 1, 2, 3, 5, 6]
扩展运算符与解构赋值结合起来,用于生成数组
代码语言:javascript复制const [first, ...rest] = [1, 2, 3, 4, 5];
// first: 1
// rest: [2, 3, 4, 5]
- 注意:扩展运算符用于数组赋值时,只能放在参数的最后一位,否则会报错
将字符串转为真正的数组
代码语言:javascript复制[...'hello']; // ['h', 'e', 'l', 'l', 'o']
任何 Iterator
接口的对象,都可以用扩展运算符转为真正的数组
function foo () {
// 用于替换 ES5 中 `Array.prototype.slice.call(arguments)` 写法
const args = [...arguments];
}
使用 Math
函数获取数组中特定的值
const arr = [1, 2, 3, 4, 5];
Math.max(...arr); // 5
Math.min(...arr); // 1
# Proxy 可以实现什么功能
在 Vue3 中通过 Proxy 来替换原本的 Object.defineProperty
实现数据响应式。
Proxy 是 ES6 新增 API,用于自定义对象中的操作。
代码语言:javascript复制// target 为要代理的对象
// handler 用来定义对象中的操作,可以用来定义 set 或 get 函数
let p = new Proxy(target, handler);
通过 Proxy 实现一个数据响应式:
代码语言:javascript复制let onWatch = (obj, setBind, getLogger) => {
let handler = {
get (target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
set (target, property, value, receiver) {
setBind(value, property);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 };
let p = onWatch(
obj,
(v, property) => {
console.log(`set ${property} to ${v}`);
},
(target, property) => {
console.log(`get ${property} = ${target[property]}`);
}
);
p.a = 2; // set a to 2
p.a; // get a = 2
# 对 JSON 的理解
JSON 是一种基于文本的轻量级数据交换格式。可以被任何的编程语言读取或作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式,在前端通过将一个符合 JSON 格式的数据序列化为 JSON 字符串,然后将其传递给后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,一次实现前后端数据传递。
因为 JSON 语法是基于 JavaScript 的,很容易将 JSON 和 JavaScript 中的对象弄混,但是应该注意 JSON 和 JavaScript 中的对象不是一回事,JSON 中对象格式更加严格,如 JSON 中属性值不能为函数,不能出现 NaN
属性值等。
JavaScript 提供了 JSON.stringify
和 JSON.parse
方法来实现 JSON 的序列化和反序列化。
# JavaScript 脚本延迟加载的方法有哪些
延迟加载即等页面加载完成后再加载 JavaScript 文件,JavaScript 延迟加载有助于提高页面加载速度。常见方法有:
defer
属性- 该属性可以让脚本加载与文档的解析同步进行,然后在文档解析完成后再执行脚本,这样可以使页面的渲染不再被阻塞
- 多个设置了
defer
属性的脚本按规范是最后顺序执行的,不过一些浏览器可能实现不同
async
属性- 该属性会使脚本异步加载,不会阻塞页面的解析过程,但当脚本加载完成后立即执行 JavaScript,这时如果文档没有解析完成的话同样会阻塞
- 多个
async
属性的脚本的执行顺序不可预测
- 动态创建 DOM 方法
- 动态创建 DOM 标签的方法,可以对文档的加载事件监听,当文档加载完成后再动态创建
<script>
标签引入 JavaScript 脚本
- 动态创建 DOM 标签的方法,可以对文档的加载事件监听,当文档加载完成后再动态创建
- 使用
setTimeout
延迟方法- 设置定时器来延迟加载 JavaScript 脚本
- 让 JavaScript 最后加载
- 将 JavaScript 脚本放在文档的底部,来使脚本尽可能在最后来执行
# DOM 和 BOM
DOM (Document Object Model),文档对象模型,指将文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
BOM (Browser Object Model),浏览器对象模型,指将浏览器当做一个对象来对待,主要定义了与浏览器进行交互的方法和接口。
- BOM 的核心是
window
对象,window
对象具有双重角色,既是通过 JavaScript 访问浏览器窗口的接口,又是一个 Global(全局)对象,即网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或方法存在。 window
对象含location
对象、navigator
对象、screen
对象等,且 DOM 的根本对象document
也是window
对象的子对象。
# 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 请求的步骤:
- 使用
open
方法创建 HTTP 请求,该方法需要参数是请求的方法、地址和是否异步及用户认证信息; - 发起请求前,可以添加一些信息和监听函数;
- 最后调用
send
向服务器发起请求,可以传入参数作为发送的数据体;
const SERVER_URL = `http://localhost:3000/api/users`;
const xhr = new XMLHttpRequest();
xhr.open(`GET`, SERVER_URL, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.log(`Error: ${xhr.status}`);
}
}
return;
};
xhr.onerror = function () {
console.log(`Error: ${xhr.status}`);
};
xhr.responseType = `json`;
xhr.setRequestHeader('Accept', 'application/json');
xhr.send();
# ES6 模块和 CommonJS 模块 异同
- 区别
- CommonJS 对模块是浅拷贝,ES6 Module 是对模块的引用,即 ES6 Module 只存只读,不能改变其值,即指针指向不能变,类似
const
; import
的接口是read-only
(只读状态),不能修改其变量值,即不能修改其变量的指针指向,当可以改变变量内部指针指向- 可以对 CommonJS 重新赋值,但是对 ES6 Module 赋值会编译报错
- CommonJS 对模块是浅拷贝,ES6 Module 是对模块的引用,即 ES6 Module 只存只读,不能改变其值,即指针指向不能变,类似
- 共同点
- CommonJS 和 ES6 Module 都可以对引入的对象进行赋值,即对对象内部属性的值进行改变
# for...in 和 for...of 的区别
for...of
是 ES6 新增的遍历方式,允许遍历一个含有 iterator
接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for...in
的区别:
for...of
遍历获取的是对象的键值,而for...in
遍历获取的是对象的键名;for...of
只遍历当前对象不会遍历原型链,for...in
会遍历对象的整个原型链,性能非常差,不推荐使用;- 对于数组的遍历,
for...in
会返回数组中所有可以枚举的属性(包括原型链上可枚举的属性),for...of
只返回数组的下标对应的属性值;
总结:
for...in
循环主要是为了遍历对象而生,不适合遍历数组for...of
循环可以用来遍历数组、类数组对象,字符串、Set、Map 及 Generator 对象
# Ajax、 axios 和 fetch 的区别
# Ajax
Asynchronous JavaScript and XML 指一种创建交互式网页应用的网页开发技术。是在一种无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。
传统的网页如果需要更新内容,需要重载整个页面。其缺点如下:
- 本身是针对 MVC 编程,不符合前端 MVVM 的浪潮
- 基于原生的 XHR 开发,XHR 本身的架构不清晰
- 不符合关注分离的原则
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好
# Fetch
Fetch 号称 Ajax 的替代品,是在 ES6 出现的,使用了 ES6 中的 Promise 对象。Fetch 是基于 Promise 设计的,其代码结构比 Ajax 简单多,它不是对 Ajax 的进一步封装,而是原生的 JavaScript,没有使用 XMLHttpRequest 。
其优点有:
- 语法简洁,更加语义化
- 基于标准 Promise 实现,支持
async/await
- 更加底层,提供的 API 丰富(request, response)
- 脱离了 XHR ,是 ES 规范里新的的实现方式
缺点有:
- 只对网络请求报错,对 400,500 都当做成功的请求,服务器返回 400,500 错误码不会
reject
,只有网络错误导致请求不能完成时,fetch 才会reject
- fetch 默认不会带 cookie,需要添加配置项:
fetch(url, {credentials: 'include'})
- fetch 不支持 abort,不支持超时控制,使用
setTimeout
及Promise.reject
的实现的超时控制并不能阻止请求过程继续在后台运行,造成了浏览浪费 - fetch 没有办法原生监测请求的进度,而 XHR 可以
# Axios
Axios 是一种基于 Promise 封装的 HTTP 客户端,其特点如下:
- 浏览器端发起 XMLHttpRequest 请求
- Node.js 端发起 HTTP 请求
- 支持 Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换 JSON 数据
- 客户端支持 XSRF 攻击
# 对原型、原型链的理解
在 JavaScript 中使用构造函数来新建一个对象的,每一个构造函数内部都有一个 prototype
属性,属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,指向构造函数的 prototype
属性对应的值,在 ES5 中这个指针称为对象的原型,可以通过 __proto__
属性来访问,但最好不要在实践中使用,因为他不是规范中规定的。ES5 中新增了 Object.getPrototypeOf()
方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里去找这个属性,这个原型对象又会有自己的原型,于是就一直找下去,即原型链。原型链的尽头一般来说都是 Object.prototype
所以这就是新建的对象为什么能使用 toString
方法的原因。
特点:JavaScript 对象是通过引用来传递的,创建的每个新对象实例中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。
# 原型链的终点是什么?如何打印出原型链的终点?
由于 Object
是构造函数,原型链终点 Object.prototype.__proto__
,而 Object.prototype.__proto__ === null
,所以,原型链的终点是 null
。
原型链上的所有原型都是对象,所有的对象最终都是由 Object
构造的,而 Object.prototype
的再上一层是 Object.prototype.__proto__
。
# 作用域 和 作用域链
- 全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有
window
对象的属性拥有全局作用域 - 全局作用域由很大的弊端,过多的全局作用域变量会污染全局命名空间,引起命名冲突
- 函数作用域
- 声明在函数内部的变量,一般只有固定的代码片段可以访问到
作用域是分层的,内层作用域可以访问外层,反之不行
- 块作用域
- ES6 中新增
let
和const
指令可以声明块级作用域 - 块级作用域可以在函数中创建,也可以在一个代码块(
{}
)中创建 let
和const
声明的变量不会有变量提升,也不可以重复声明- 在循环中比较适合绑定块级作用域,可以将声明的计数器变量限制在循环内
- ES6 中新增
- 作用域链
- 在自己作用域中找不到变量就去父级作用域查找,依次向上级作用域查找,直到访问到全局作用域就终止,这一层层关系就是作用域链
- 作用域链保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数
- 本质上是一个指向变量对象的指针列表,变量对象是一个包含了执行环境中所有变量和函数的对象
- 作用域链的前端始终都是当前执行上下文的变量对象,全局执行上下文的变量对象始终是作用域链的最后一个对象
- 当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找
# this
this
是执行上下文中的一个属性,指向最后一次调用这个方法的对象。在实际开发中,this
的指向可以通过四种调用模式来判断:
- 函数调用模式:当一个函数不是一个对象的属性时,直接作为函数来调用时,
this
指向全局对象 - 方法调用模式:如果一个函数作为一个对象的方法来调用时,
this
指向调用这个方法的对象 - 构造器调用模式:如果一个函数用
new
调用时,函数执行前会新创建一个对象,this
指向这个新创建的对象 apply
、call
和bind
调用模式:显式指定调用函数的this
指向。apply
,接收两个参数,一个是this
的绑定对象,一个是参数数组call
,第一个参数是this
的绑定对象,后面的其余参数是传入函数执行的参数bind
,传入一个对象,返回一个this
绑定了传入对象的新函数,这个函数的this
指向除了使用new
时会被改变,其他情况下都不会改变
以上四种方式,使用构造器调用模式的优先级最高,然后是显式指定方法,然后是方法调用模式,然后是函数调用模式。
# 异步编程的实现方式
JavaScript 中异步机制可以分以下几种:
- 回调函数
- 多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护
- Promise
- 使用 Promise 可以将嵌套的回调函数转为链式调用
- 使用这种方法,有时会造成多个
then
的链式调用,可能会造成代码的语义不够明确
- Generator
- 可以在函数的执行过程中,将函数的执行全转移出去,在函数外部还可以将执行权转移回来
- 当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来
- Generator 内部对于异步操作的方式,可以以同步的顺序来书写,使用这种方式需要考虑何时将函数的控制权转移回来,需要一个自动执行 Generator 的机制
- Async/Await
async
函数是Generator
和Promise
实现的一个自动执行的语法糖- 内部自带执行器,当函数内部执行到一个
await
语句时,如果语句返回一个Promise
对象,那么函数将会等待Promise
对象的状态变为resolve
后在继续向下执行。因此,可以将异步逻辑转化为同步的顺序来书写,并且这个函数可以自动执行
# 对 Promise 的理解
Promise 是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,它的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
Promise 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作可以用同样的方法进行处理。
Promise 实例有三种状态:
pending
,进行中resolved
,已完成rejected
,已拒绝
当把一件事交给 Promise 时,其状态就是 pending
,任务完成了就变成 resolved
,任务失败了就变成 rejected
。
Promise 实例有两个过程:
pending -> fulfilled: resolved
,已完成pending -> rejected: rejected
,已拒绝
注意:一旦从进行状态变为其他状态就永远不能更改状态了。
Promise 的特点:
- 对象状态不受外界影响
- Promise 对象代表一个异步操作,有三种状态:
pending
,resolved
和rejected
- 只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是其名称的由来
- Promise 对象代表一个异步操作,有三种状态:
- 一旦状态改变就不会再变,任何时候都可以得到这个结果
- Promise 对象的状态改变,只有两种可能:从
pending
变为resolved
或从pending
变为rejected
- 如果改变已经发生了,再对 Promise 对象添加回调函数,也会立即得到这个结果
- 与事件(Event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的
- Promise 对象的状态改变,只有两种可能:从
Promise 的缺点:
- 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,Promise 内部错误抛出,不会反应到外部
- 当处于
pending
状态时,无法得知目前进展到哪一阶段(是刚刚开始还是即将完成)
总结: Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态:pending
、resolved
、rejected
。实例的状态只能由 pending
变为 resolved
或 rejected
,并且状态一经改变,就固定了,无法再被改变了。
状态的改变时通过 resolve()
和 reject()
来实现,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then
方法,使用这个 then
方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮时间循环的末尾执行。
注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的。
# Promise 解决了什么问题
传统的多任务异步写法:
代码语言:javascript复制let fs = require('fs');
fs.readFile('./test.txt', 'utf8', function(err, data1) {
fs.readFile(data1, 'utf8', function(err, data2) {
fs.readFile(data2, 'utf8', function(err, data3) {
console.log(data3);
});
});
});
上面的代码有如下缺点:
- 后一个异步任务的参数依赖于前一个异步任务成功后,将数据往下传递,会导致多个异步函数嵌套的情况,代码不够直观
- 如果前后两个异步任务不需要传递参数的情况下,那后一个异步任务也需要前一个成功后再执行下一步操作,这种情况下,也需要嵌套,代码不够直观
Promise 的写法:
代码语言:javascript复制let fs = require('fs');
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
read('./test.txt').then(data1 => {
return read(data1);
}).then(data2 => {
return read(data2);
}).then(data3 => {
console.log(data3);
}
使用 Promise 链式调用,可以顺利解决地狱回调的问题
# 对 Async/Await 的理解
async / await
其实是 Generator 的语法糖,它能实现的洗锅都能用 then
链来实现,它是为了优化 then
链出现的。从字面上看,async
即 “异步”,await
即 “等待”,所以很好地理解 async
用于申明一个 function
是异步的,而 await
用于等待一个异步方法执行完成。语法上强制规定 await
只能出现在 async
函数中。
async function f() {
return 1;
}
let result = f();
console.log(result); // Promise { 1 }
async
函数(包括函数语句、函数表达式、Lambda 表达式)返回的是一个 Promise
对象,如果函数中 return
一个直接量,async
会把这个直接量通过 Promise.resolve()
封装成 Promise
对象。
async
函数返回的是一个 Promise 对象,所以在最外层不能用 await
获取其返回值的情况下,当然应该用原来的方式: then()
链来处理这个 Promise 对象。
async function f() {
return 1;
}
let result = f();
result.then(data => {
console.log(data); // 1
};
如果 async
函数没有返回值,会返回 Promise.resolve(undefined)
。
在没有 await
的情况下执行 async
函数,它会立即执行,返回一个 Promise 对象,并且不会阻塞后面的语句,这和普通返回 Promise 对象的函数没有区别。
Promise.resolve(x)
可以看做是new Promise(resolve => resolve(x))
的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
# async / await 的优势
单一的 Promise 链并不能发现 async / await
的优势,但是,如果需要处理多个 Promise 组成的 then
链的时候,优势就能体现出来了。(Promise 通过 then
链来解决多层回调的问题,async / await
又进一步优化 then
链的问题)
相对于 Promise 有以下优势:
- 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是
then
的链式调用也会带来额外的阅读负担 - Promise 传递中间值非常麻烦,而
async / await
几乎是同步的写法,非常优雅 - 错误处理友好,
async / await
可以用成熟的try / catch
方式处理错误,Promise 的错误处理非常冗余 - 调试友好,Promise 的调试很差,由于没有代码块,不能在一个返回表达式的箭头函数中设置断点
- 如果你启图在
.then
代码块中使用调试器的 Step-Over 功能,调试器并不会进入后续的.then
代码块,因为调试器只能跟踪同步代码的每一步
- 如果你启图在
# 对象创建的方式有哪些
一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。JavaScript 和一般的面向对象的对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生可以复用的对象创建方式,常见:
- 工厂模式
- 主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的
- 有个问题就是创建出来的对象无法和某个类型联系起来,只是简单封装了代码,没有建立对象和类型间的关系
- 构造函数模式
- JavaScript 中每一个函数都可以作为构造函数,只要一个函数通过
new
来调用,就可以称其为构造函数 - 执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的
prototype
属性,然后将执行上下文的this
指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为this
的值指向了新建的对象,所以可以使用this
给对象赋值。 - 构造函数模式相对于工厂模式
- 优点:创建的对象和构造函数建立了联系,可以通过原型来识别对象的类型
- 缺点:造成了不必要的函数对象的创建,因为 JavaScript 中函数也是一个对象,如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间
- JavaScript 中每一个函数都可以作为构造函数,只要一个函数通过
- 原型模式
- 每一个函数都有一个
prototype
属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法 - 可以通过使用原型对象来添加公共属性和方法,从而实现代码的复用,解决了函数复用的问题
- 也存在一些问题
- 没有办法通过传入参数来初始化值
- 如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例
- 每一个函数都有一个
- 组合使用构造函数模式和原型模式
- 这是创建自定义类型最常见的方式
- 通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用
- 这种方法很好解决了两种模式单独使用时的缺点,但是也有缺点,因为使用了两种不同的模式,所以对于代码的封装性不够好
- 动态原型模式
- 将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果
- 很好地对上面的混合模式进行了封装
- 寄生构造函数模式
- 和工厂模式的实现相似
- 缺点是无法对对象识别
# 对象继承的方式有哪些
- 原型链继承
- 借用构造函数继承
- 组合继承:将原型链和借用构造函数组合起来使用
- 原型式继承
- 寄生式继承
- 寄生组合继承
# 哪些情况会导致内存泄露
- 意外的全局变量
- 由于使用未声明的变量,而意外创建了一个全局变量,而使这个变量一直留在内存中无法被回收
- 被遗忘的计时器或回调函数
- 设置了
setInterval()
定时器,忘记取消,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存,而无法被回收
- 设置了
- 脱离 DOM 的引用
- 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以其无法被回收
- 闭包
- 不合理使用闭包,从而导致某些变量一直被留在内存当中