一、数据类型
1. JavaScript有哪些数据类型,它们的区别?
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol代表创建独一无二不可变更的数据类型,主要解决全局变量冲突问题
- BigInt是数据类型的数据,可以表示任意精度格式的整数,使用它可以安全地存储和操作大整数,即使这个数超过了Number能代表的整数范围
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:应用数据类型(对象、数组和函数)
2. 数据类型检测的方式有哪些
(1)typeof
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
(2)instanceof
instanceof可以正确判断对象的类型,运行机制是判断其原型链中能否找到该类型的原型
console.log(2 instanceof Number); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function) //true
console.log({} instanceof Object); // true
(3)contructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(([]).constructor===Array) // true
console.log((function(){}).constructor===Function) //true
console.log(({}).constructor === Object); // true
(4)Object.prototype.toString.call()
console.log(Object.prototype.toString.call(2)) // '[object Number]'
console.log(Object.prototype.toString.call({})) // '[object Object]'
console.log(Object.prototype.toString.call(undefined)) // '[object Undefined]'
console.log(Object.prototype.toString.call(null)) // '[object Null]'
3、判断数组的方法
- 通过Object.prototype.toString.call()判断
Object.prototype.toString.call(obj)==='[object Array]'
- 通过原型链判断
obj.__proto__===Array.prototype
- 通过ES6的Array.isArray(obj)
- 通过instanceof做判断 obj instanceof Array
- 通过Array.prototype.isPrototypeOf(obj)
4、object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
拓展运算符
let outObj={
inObj:{a:1,b:2}
}
let newObj={...outObj}
newObj.inObj.a=2
console.log(outObj) // {inObj: {a: 2, b: 2}}
object.assign
let outObj={
inObj:{a:1,b:2}
}
let newObj3=Object.assign({},outObj)
newObj.inObj.a = 3
console.log(newObj3) // {inObj: {a: 3, b: 2}}
可以看到,两者都是浅拷贝。
二、es6
1.let var const 区别
| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | 无 | 有 | 有 |
| 是否有变量提升 | 有 | 无 | 无 |
| 能否重复声明 | 能 | 否 | 否 |
| 是否必须设置初始值 | 否 | 否 | 是 |
| 是否存在暂时性死区 | 无 | 有 | 有 |
| 能否改变指针指向 | 能 | 能 | 否 |
2.const对象的属性可以修改吗
const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。 对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量;
引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了
3、如果new一个箭头函数的会怎么样
箭头函数没有prototype,没有自己的this指向,所以不能new一个箭头函数 new操作符的实现步骤: 1.创建一个对象
2.将构造函数的作用域赋予给新函数(也就是将对象的__proto__属性指向构造函数的prototype属性)
3.构造函数中的this指向该对象(即给该对象添加属性和方法)
4.返回新对象
4、箭头函数和普通函数区别
(1)箭头函数没有自己的this
箭头函数继承来自自己作用域的上一层,它的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'
(2)call()、apply()、bind()等方法不能改变箭头函数中this的指向**
(3)箭头函数没有自己的arguments
(4)箭头函数没有prototype
5.扩展运算符的作用及使用场景
(1)对象扩展运算符
对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }
上述方法实际上等价于:
let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }
Object.assign用于对象的合并,将源对象(source)的所有的枚举属性,复制到目标对象(target)。Object.assign的第一个参数为目标对象,后面的所有参数都是源对象 (如果目标对象中有和源对象相同的属性,或多个源对象有同名属性,后面的属性会覆盖前面的属性)
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组
console.log(...[1, 2, 3]) // 1 2 3
console.log(...[1, [2, 3, 4], 5]) // 1 [2, 3, 4] 5
- 将数组转换为参数序列
function add(x, y) { return x + y; }
const numbers = [1, 2];
add(...numbers) // 3
- 扩展运算符与解构赋值结合起来,用于生成数组
const [first,...rest]=[1,3,5,7,9] //first 1 //rest [3,5,7,9]
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [...rest, last] = [1, 2, 3, 4, 5]; //报错
6. 如何提取高度嵌套的对象里的指定属性?
const school = { classes: { stu: { name: 'Bob', age: 24, } } }
const { classes: { stu: { name } }} = school
console.log(name) // 'Bob'
通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。
7.对 rest 参数的理解
它还可以把一个分离的参数序列整合成一个数组:
function mutiple(...args) {
let result = 1;
for (var val of args) {
result *= val;
}
return result;
}
mutiple(1, 2, 3, 4) // 24
这就是 … rest运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。
8. ES6中模板语法与字符串处理
var name = 'css' var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
- includes:判断字符串与子串的包含关系:
const son='world'
const father='hello world okk'
father.includes(son) //true
- startsWith:判断字符串是否以某个/某串字符开头:
const son='hello'
const father='hello world okk'
father.startsWith(son) //true
三、JavaScript基础
1.Map的理解
Map是键值对的集合,它的键可以为任意类型,实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:
const map = [
["name","张三"],
["age",18],
]
Map数据结构有以下操作方法:
- size: 返回Map结构总成员数
- set(key,value) 设置键名key对应的value值,返回整个Map结构
- get(key) 获取key对应的键值
- has(key) 判断某个键是否在该Map结构中
- delete(key) 删除某个键值
- clear() 清除所有成员
Map结构原生提供是三个遍历器生成函数和一个遍历方法
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历Map的所有成员。
const map = new Map([
["foo",1],
["bar",2],
])
for(let key of map.keys()){
console.log(key); // foo bar
}
for(let value of map.values()){
console.log(value); // 1 2
}
for(let items of map.entries()){
console.log(items); // ["foo",1] ["bar",2]
}
map.forEach( (value,key,map) => {
console.log(key,value); // foo 1 bar 2
})
2.对类数组对象的理解,如何转化为数组
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有 length属性值,代表可接收的参数个数
常见的类数组转换为数组的方法有这样几种:
- 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike)
- 通过 Array.from 方法来实现转换
Array.from(arrayLike);
3.如何判断一个对象是否属于某个类?
- 使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置
- 通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写
- 如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断
4.for...in和for...of的区别
for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。
5.forEach和map方法有什么区别
- forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
- map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
四、原型与原型链
1.对原型、原型链的理解
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
2.原型修改、重写
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
3. 原型链指向
p.__proto__ // Person.prototype
Person.prototype.__proto__ //Object.prototype
p.__proto__.constructor //Person
Person.prototype.constructor //Person
4.原型链的终点是什么?如何打印出原型链的终点?
由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null
5. 如何获得对象非原型链上的属性?
使用后hasOwnProperty()方法来判断属性是否属于原型链的属性
function iterate(obj){
let res=[]
for(var key in obj){
if(obj.hasOwnProperty(key)){
res.push(key+':'+obj[key])
}
}
return res
}
五、执行上下文/作用域链/闭包
1. 对闭包的理解
闭包指有权访问另外一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途;
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A(){
let i=1
window.B=function(){
console.log(i)
}
}
A()() //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=0;i<=5;i++){
(function(j){
setTimeout(function(){
console.log(j)
},1000)
})(i)
}
- 使用
let定义i了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
2. 对作用域、作用域链的理解
1)全局作用域和函数作用域
(1)全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有window对象的属性拥有全局作用域
(2)函数作用域
- 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
- 作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
- 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由
{ }包裹的代码片段) - 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链
六、this/call/apply/bind
1. 对this对象的理解
this是执行上下文中的一个属性,指向最后一次调用这个方法的对象,this指向的判断方法有下:
- 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
- 第二种是方法调用模式,函数作为一个对象的方法被调用时候,this指向该对象
- 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
- 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
七、异步编程
javascript异步机制分以下几种:
- 回调函数的方式,缺点是多个回调函数嵌套会造成地狱回调,上下两层的回调函数间代码耦合度高,不利于代码维护
- Promise的方式,可以把多个回调函数组合成链式调用,
- generator 的方式,它可以在函数的执行过程中,将函数的执权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
- async方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,函数将会等待Promise对象的状态变为resolve后继续向下执行,所以可以把异步逻辑转为同步的顺序来书写。
1.### setTimeout、Promise指向顺序
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
2. 对Promise的理解
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
(1)Promise的实例有三个状态:
- Pending(进行中)
- Resolved(已完成)
- Rejected(已拒绝)
状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
setTimeout、Promise、Async/Await 的区别
(1)setTimeout
console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout
(2)Promise
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当JS主线程执行到Promise对象时:
- promise1.then() 的回调就是一个 task
- promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
- promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
- setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
(3)await/async
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end
3.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);
}
});
下面是使用resolve方法和reject方法:
function testPromise(ready) {
return new Promise(function(resolve,reject){
if(ready) {
resolve("hello world");
}else {
reject("No thanks");
}
});
};
// 方法调用
testPromise(true).then(function(msg){
console.log(msg);
},function(error){
console.log(error);
});
(2)Promise方法
Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。
- then
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=>{
})
- catch
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
all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。
function testPromiseAll1(bool) {
return new Promise((resolve, reject) => {
if (bool) {
setTimeout(() => {
resolve('成功1')
}, 3000);
} else {
setTimeout(() => {
reject("失败1")
}, 3000);
}
})
}
function testPromiseAll2(bool) {
return new Promise((resolve, reject) => {
if (bool) {
setTimeout(() => {
resolve('成功2')
}, 1000);
} else {
setTimeout(() => {
reject("失败2")
}, 1000);
}
})
}
function testPromiseAll3(bool) {
return new Promise((resolve, reject) => {
if (bool) {
setTimeout(() => {
resolve('成功3')
}, 2000);
} else {
setTimeout(() => {
reject("失败3")
}, 2000);
}
})
}
Promise.all([testPromiseAll1(true), testPromiseAll2(true), testPromiseAll3(false)]).then(res => {
console.log('all的res', res)
})
调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。
- race
接受的参数是一个每项都是
promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。
function testPromiseAll1(bool) {
return new Promise((resolve, reject) => {
if (bool) {
setTimeout(() => {
resolve('成功1')
}, 3000);
} else {
setTimeout(() => {
reject("失败1")
}, 3000);
}
})
}
function testPromiseAll2(bool) {
return new Promise((resolve, reject) => {
if (bool) {
setTimeout(() => {
resolve('成功2')
}, 1000);
} else {
setTimeout(() => {
reject("失败2")
}, 1000);
}
})
}
function testPromiseAll3(bool) {
return new Promise((resolve, reject) => {
if (bool) {
setTimeout(() => {
resolve('成功3')
}, 2000);
} else {
setTimeout(() => {
reject("失败3")
}, 2000);
}
})
}
Promise.race([testPromiseAll1(true), testPromiseAll2(true), testPromiseAll3(false)]).then(res => {
console.log('all的res', res)
})
- finally
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
4.Promise解决什么问题
在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:
let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})
上面的代码有如下缺点:
- 后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。
- 如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观
Promise出现之后,代码变成这样:
let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})
这样代码看起了就简洁了很多,解决了地狱回调的问题。
5.对async/await 的理解
async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:
async function testAsyc1() {
return new Promise(resolve => {
resolve('hello')
})
}
async function testAsyc2() {
return 'something'
}
function testAsyc3() {
return new Promise(resolve => {
setTimeout(() => {
resolve('hello world')
}, 2000);
})
}
async function dosomething() {
let rs1 = await testAsyc1()
let rs2 = await testAsyc2()
let rs3 = await testAsyc3()
console.log('rs1', rs1)
console.log('rs2', rs2)
console.log('rs3', rs3)
}
dosomething()
await 表达式的运算结果取决于它等的是什么。
- 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
- 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
6.async/await的优势
如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在用 Promise 方式来实现这三个步骤的处理:
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
如果用 async/await 来实现呢,会是这样:
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
7.async/await对比Promise的优势
-
代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
-
Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
-
错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
-
调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。
八、面向对象
1. 对象创建的方式有哪些?
一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:
(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。 (3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
2.对象继承的方式有哪些?
(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
(4)第4种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用
(5)第5种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。