前言
上篇博客,我们了解了javascript函数执行过程,本篇章主要讲述函数,如参数、回调、同/异步、箭头函数等,并阐述了对ES6中class类的认识以及继承。
面试回答
1.ES6新特性:新增了一种值类型Symbol以及Set和Map的数据结构。增加了块级作用域,比如let,const。增加了变量的解构赋值。新增了扩展运算符。提供了类的语法糖class。新增了箭头函数。函数参数允许设置默认值,引入了rest参数,新增了一些API,比如isArray、keys()。
2.异步编程史:异步编程有四个阶段,分别是回调函数、Promise、Generator、async/await,回调函数的缺点在于容易出现回调地狱,不能用try/catch捕获错误,不能return。Promise构造函数内的代码是同步执行,then方法中的代码是异步执行的,可以通过构造函数的参数reslove进入then回调中,不过缺点是无法取消,错误需要回调来捕获。Generator可以用来控制函数,比如暂停、恢复执行。async/await是Promise的语法糖,它是为优化then链而开发出来的,解决了then链过长的问题,await的功能基本等同于then,它能实现的效果都能用then来实现,只不过then方法是微任务,属于异步任务,而await后的方法等同于Promise的构造函数,这个方法之后的代码等同于then回调。
3.箭头函数:箭头函数没有原型,所以不能作为构造函数。它的this指向为当前所在环境的this,且箭头函数不支持重命名参数,也没有arguments对象。
4.闭包:闭包能够使内部函数读取外部函数的变量,让变量长期驻扎在内存当中,从而让该变量不被垃圾回收机制回收,当不再需要闭包时,把内部函数赋值为null即可。好处是能够避免因为作用域问题而把变量定义在全局作用域里面,造成全局变量的污染。闭包的原理是利用了函数作用域链的特性,内部函数会将外部函数的活动对象添加到作用域链里,当外部函数执行完毕,作用域链会被销毁,但活动对象由于仍被内部函数的作用域链引用,所以不会被销毁。现在闭包很少用到,以前的话会用闭包解决封装模块以及循环中作用域的问题。
5.手写Promise:Promise有状态、构造函数、then方法、catch方法、finally方法、all等方法,首先定义三个状态,pending、resolved、rejected以及一些变量,比如status、data、reason、callback用来储存状态、resolve返回、reject返回和then方法的回调。然后实现构造函数以及reject、resolved两个方法,构造函数一般用try/catch方法直接接收或抛出,reject、resolved这两个方法是创建实例的时候作为构造函数的参数传入,且两个方法都包含对状态的判断以及修改,并且处理回调。接着实现then方法,由于then方法是微任务且最后会返回一个Promise,所以我们要new一个新实例然后判断状态,再执行queueMicrotask方法,最后就是一些catch方法、finally等方法的处理,这两个都可以通过直接返回之前实现的then方法实现,只不过finally不接收参数。Promise的all方法,同样最后会返回一个Promise,不同的是Promise.all方法接收的参数是数组,可能包含多个请求,需要用let...of...对参数进行遍历,遍历后判断它是否为Promise对象,如果是则直接调用then方法,如果不是则将参数保存到结果变量中,最后一起返回这个结果变量。
6.闭包:闭包能够使内部函数读取外部函数的变量,让变量长期驻扎在内存当中,从而让该变量不被垃圾回收机制回收,当不再需要闭包时,把内部函数赋值为null即可。好处是能够避免因为作用域问题而把变量定义在全局作用域里面,造成全局变量的污染。闭包的原理是利用了函数作用域链的特性,内部函数会将外部函数的活动对象添加到作用域链里,当外部函数执行完毕,作用域链会被销毁,但活动对象由于仍被内部函数的作用域链引用,所以不会被销毁。现在闭包很少用到,比较典型的就是vue中的data数据,以前的话会用闭包解决封装模块以及循环中作用域的问题。
知识点
1.参数
JavaScript 函数有个内置的对象,arguments对象,argument对象包含了函数调用时的由所有参数组成的数组,arguments对象可以应用于函数调用时参数太多(超过声明)的情景。
function test() {
let arg = arguments
if(arg.length !== 0) console.log(arg)
}
test(1, 123, 500, 115, 44, 88)
参数默认值
function test(x, y = 10) {
return x + y;
}
test(0, 2) // 2
test(5) //15
rest参数(...)
function test(a, ...b) {
console.log(a)
console.log(b)
}
test(1, 2, 3, 4, 5)
//1
//[ 2, 3, 4 ,5]
2.Generator函数
yield是JS为了解决异步调用的命令。表示程序执行到这里会交出执行权,等待结果返回。它需要在Generator函数中运行,与普通function的区别就是函数名前面多了一个星号*
,这里只做最简单的用法,有兴趣的同学可以继续研究~
function *generatorForLoop(num) {
for (let i = 0; i < num; i += 1) {
yield console.log(i);
}
}
const genForLoop = generatorForLoop(5);
genForLoop.next(); // 0
genForLoop.next(); // 1
genForLoop.next(); // 2
genForLoop.next(); // 3
genForLoop.next(); // 4
3.箭头函数
箭头函数没有原型,本身没有this,在箭头函数中使用this,它的指向为当前所在环境的this,所以不可以当作构造函数,new一个箭头函数也会抛出一个错误。箭头函数不支持重命名函数参数,关于箭头函数的this指向问题已经在上一篇博客中理解过了,这边就不着重描述了,我们分为局部函数环境、全局环境来看。
全局环境
全局环境下,箭头函数的this都会指向window,使用aguments会报错,举例:
let foo = ()=>{console.log(this)};
foo(); //window
局部函数环境
根据上一篇this指向的理解,在局部函数环境,箭头函数的this指向它的外层普通函数时,它的arguments指向外层普通函数的arguments,取而代之用rest参数...
。箭头函数本身的this指向不能改变,也不能使用call、apply去改变,但改变它所在的上下文(也就是外层函数)的this指向,举例:
//情况一:下面的箭头函数this的指向是function b (固定的),而function b的this指向根据普通函数的this指向规则,是指向调用它的对象,也就是obj,所以箭头函数--> function b --> obj,也就是箭头函数的this指向,指向obj,obj.a=2
var a = 1
var obj = {
a:2,
b:function(){
return ()=>{
console.log(this.a)
}
}
}
obj.b()() //2
//情况二:这时候想要改变箭头函数this指向,可以在箭头函数外包一层匿名函数即可(匿名函数默认指向window),所以关于箭头函数的this指向在创建阶段确定,且无法改变的说法是没问题的。
var a = 1
var obj = {
a:2,
b:function(){
return function(){
return ()=>{
console.log(this.a)
}
}
}
}
obj.b()()() //1 obj.b()等于return的function,而obj.b()()等于调用return的function即window.b(),所以指向window
//情况三:或者直接使用call改变外层函数的this指向
var a = 1
var obj = {
a:2,
b:function(){
return ()=>{
console.log(this.a)
}
}
}
obj.b.call(window)() //1
4.class类
先来看一下ES5的类,
function Animal(word,food){
this.word = word
this.food = food
this.eat = function(){
console.log('I eat',this.food)
}
}
再看一下ES6完整的例子,记得有疑问的地方要标记一下哦:
class Animal {
//#privateAnimal = 'Animal Animal' //参考A.1.私有成员
constructor(word, food) { //参考A.2.构造函数
this.word = word
this.food = food
}
static say(word){ //参考A.1.静态成员
console.log(word)
}
eat(){ //参考A.1.公有成员
console.log('I eat',this.food)
}
testSuper(){ //参考A.2.构造函数super
console.log('super')
}
//#privateCon(){ //参考A.1.私有成员
//console.log(this.#privateCat)
//}
}
class Cat extends Animal { //参考A.4.继承
constructor(word, food) {
super() //参考A.2.构造函数super
this.word = word
this.food = food
}
['play'+'bool'](){ //参考A.3.计算属性名称
console.log('Cat play bool')
}
//get、set 参考A.3.访问器属性
get age(){
return 'getter'
}
set age(val){
console.log('setter',val)
}
getSuper(){ //参考A.2.构造函数super
console.log(super.testSuper())
}
}
//参考A.1.静态成员
Animal.say('Hello Animal')
Cat.say('Hello Cat')
//参考A.1.私有成员
//Animal.#privateCon() // error,不可访问
let animal = new Animal('Hello Animal','meet');
animal.eat()
let cat = new Cat('Hello Cat','fish');
cat.eat()
cat.playbool()
//参考A.2.构造函数super
cat.getSuper()
//get、set 参考A.2.访问器属性
cat.age = 3
cat.age
A.1.基础概念
成员:成员分为私有成员、公有成员以及静态成员,无论属性还是函数只要加上关键字都可以成为对应的成员。
私有成员:关键字#
,私有成员有以下几个特点,
- class内部不同方法间可以使用,因此this要指向实例化对象;
- 不能被外部访问,因此实例化对象既不能获得值,也不能设定值;
- 不能被继承,因此extends后子类不具备该属性
静态成员:是指在方法名或属性名前面加上static
关键字,和普通方法不一样的是,静态方法不能在实例中访问,只能在类中访问;静态成员也可以通过派生类访问,但不能通过派生类的实例访问,如果静态方法包含this关键字,这个this指的是类,而不是实例。
公有成员:除去私有成员,静态成员,其他成员属于公有成员。
派生类:通过extends关键字来实现继承的功能,如Animal是Cat的基类,Cat是Animal 的派生类,Cat继承了Animal的基本能力,在派生类中定义重名函数会覆盖掉基类中的原始函数。
原型链:之前已经对原型链的概念进行理解,Class类同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。 大致理解如下:
1.子类实例(cat)的__proto__
属性,总是指向cat原型,而cat原型的__proto__
属性总是指向父类(Animal)的原型,父类实例(animal)的__proto__
属性则也是指向他自身的原型。也就是说,cat.__proto__.__proto__===animal.__proto__
。子类实例的原型的原型,是父类实例的原型。
2.子类(Cat)prototype
属性指向的是他自身的原型,然后子类(Cat)原型的原型又是指向父类的原型,也就是说,Cat.prototype.__proto__=== Animal.prototype,
即总是指向父类(Animal)的prototype
属性。
如若还有疑问,可以参考博客文章四。
A.2.构造函数
Cat类中可以看到有一个constructor
方法,这个就是构造函数。constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有定义,constructor方法会被默认添加。
在派生类中,如果使用了构造方法,且用到了this,就必须使用super(),且super()也只能在派生类中使用,在构造函数中访问之前一定要调用super(),它此时代表父类的构造函数,负责this的初始化。super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
A.3.函数
访问器属性:类支持在原型上定义访问器属性。尽管应该在类的构造函数中创建自己的属性,但是类也支持直接在原型上定义访问器属性。创建getter时,需要在关键字get后紧跟一个空格和响应的标识符;创建setter时,只需把关键字get替换为set即可。
计算属性名称:类和对象字面量有很多相似之处,类方法和访问器属性也支持使用可计算名称,用方括号包裹一个表达式,即使用计算名称。
A.4.继承
extends关键字后只要是一个有prototype属性的函数,就能被继承,至于原型链相关的知识点这里就不赘述了,不清楚的同学可以参考博客四,这里顺便聊一下JS中常见的继承方式。
原型链继承:父类的实例作为子类的原型,优点是简单易于实现,父类的新增的实例与属性子类都能访问;缺点是可以在子类中增加实例属性,如果要新增加原型属性和方法需要在new父类构造函数的后面,无法实现多继承,创建子类实例时,不能向父类构造函数中传参数。
function Cat(){
}
Cat.prototype= new Animal()
let Cat = new Cat()
构造继承:复制父类的实例属性给子类,优点是解决了子类构造函数向父类构造函数中传递参数,可以实现多继承(call或者apply多个父类);缺点是方法都在构造函数中定义,无法复用,不能继承原型属性/方法,只能继承父类的实例属性和方法。
function Cat(){
//继承了Animal
Animal.call(this)
}
let cat = new Cat();
组合继承(原型链+构造函数):调用父类构造函数,继承父类的属性,通过将父类实例作为子类原型,实现函数复用。优点是函数可以复用,不存在引用属性问题,可以继承属性和方法,并且可以继承原型的属性和方法;缺点是由于调用了两次父类,所以产生了两份实例。
function Aniaml(food){
this.food = food
}
Aniaml.prototype.eat = function(){
return this.food
}
function Cat(food){
Aniaml.call(this,food)
}
Cat.prototype = new Aniaml();
Cat.prototype.constructor = Cat;
let cat = new Cat('fish');
cat.eat();
寄生组合继承:通过寄生的方式来修复组合式继承的不足,完美的实现继承
function Animal(food,age){
this.food = food || ''
}
Animal.prototype.eat = function(){
return this.food
}
function Cat(food){
//继承父类属性
Animal.call(food)
}
//继承父类方法
(function(){
// 创建空类
let super = function(){}
super.prototype = Animal.prototype
//父类的实例作为子类的原型
Cat.prototype = new super()
})();
//修复构造函数指向问题
Cat.prototype.constructor = Cat
let cat = new Cat()
实例继承:优点是不限制调用方式,简单,易实现;缺点是不能多次继承 。
function Cat(){
let animal = new Animal()
return animal
}
let cat = new Cat()
ES6继承方式:最优
class Animal{
constructor(food=''){
this.food = food
}
eat(){
console.log(this.food)
}
}
//继承父类
class Cat extends Animal{
constructor(food = 'meet'){
//继承父类属性
super(food);
}
eat(){
//继承父类方法
super.eat()
}
}
let cat=new Cat('fish');
cat.eat();
5.Promise
Promise构造函数内是同步任务,then方法中属于异步任务的微任务。Promise的状态有三个值:pending、resolve、rejected,且Promise的状态只会发生一次改变。接下来,我们先看一下Promise的基础用法:
new Promise((reslove,reject)=>{
console.log(1)
reslove(2)//reslove后会进入then
reject(3)//reject后会进入catch,但Promise的状态只会改变一次,所以这行不会执行,但是下面那行还是会打印。
console.log(4)
}).then(res=>{
console.log(res)
}).catch(error=>{
console.log(error)
})
Promise.all():用于多次请求,不过如果出现一个接口请求异常,整个Promise.all就会被认定为失败,进入catch回调。
//getData1,getData2,getData3分别为三个接口请求
Promise.all([getData1,getData2,getData3]).then(res=>{
const [res1,res2,res3] = res //用到了解构数组
})
Promise.race():与Promise.all()返回全部数据不同,Promise.race()上面代码中,只要有一个实例率先改变状态,状态就跟着改变,返回的为率先改变的Promise实例的返回值。
const a = new Promise((resolve,reject) => {
setTimeout(() => {
console.log("a")
resolve(1)
},1000)
})
const b = new Promise((resolve,reject) => {
setTimeout(() => {
console.log("b")
resolve(2)
},2000)
})
Promise.race([a,b]).then(v => {
console.log(v)
})
// 输出:
a
1
b
1.Promise执行顺序
先看一下常见的Promise所对应的面试题来了解执行顺序:
console.log(1)
setTimeout(()=>{
console.log(6)
},0)
new Promise((reslove,reject)=>{
console.log(2)
reslove('zxp')
//reject(2)
}).then(res=>{
console.log(3)
console.log(res)
}).then(res=>{
console.log(4)
}).catch(err=>{
console.log(5)//没报错,不打印
console.log(err)
})
setTimeout(()=>{
console.log(7)
},0)
//1 2 3 zxp 4 6 7
2.手写promise
手写Promise是高频面试题,特别是在笔试题中,同时通过手写Promise也有助于我们进一步理解认识Promise,参考资料:https://juejin.cn/post/7175516630972104760#heading-6
class myPromise {
constructor(func) {
//1.定义状态以及正确、异常的返回及回调,状态对应pending、fulfilled、rejected
this.state = 'pending'
this.data = undefined
this.reason = undefined
this.resolveCallbacks = []
this.rejectCallbacks = []
// 2.实现构造函数,并传入自定义的resolve、reject
try {
func(this.resolve, this.reject)
} catch (e) {
this.reject(e)
}
}
//3.实现reslove和reject的,两者均是先判断是否为pending状态,然后更改为对应状态、赋值返回、执行所有的回调(可以通过while+数组长度+shift来实现),不同的是返回和回调不同。
resolve = (data) => {
// 一旦状态改变,就不能再变
if (this.state === 'pending') {
this.state = 'fulfilled'
this.data = data
// 依次调用成功回调,直到successCallback为空
while (this.resolveCallbacks.length) {
// 删除successCallback第一个数组元素,并执行这个被删除的元素,并传入参数。(arr.shift()返回值为被删除的元素)
this.resolveCallbacks.shift()()
}
}
}
reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
while (this.rejectCallbacks.length) {
this.rejectCallbacks.shift()()
}
}
}
//4.实现then回调:判断参数是否为函数,返回新的Promise来支持链式调用,判断状态后,用queueMicrotask方法实现微任务的执行,执行内容为调用传入的resolve或reject,然后调用resolvePromise方法。
then(onResolve, onRejected) {
//判断是否为函数,若不是则添加默认函数
onResolve = typeof onResolve === 'function' ? onResolve : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
//返回一个新的promise,来支持链式调用。
const newPromise = new myPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
queueMicrotask(() => {
try {
const value = onResolve(this.data)
//传入的参数分别代表,最新的Promise,最新的值,最新的resolve,最新的reject
this.resolvePromise(newPromise, value, resolve, reject)
} catch (e) {
reject(e)
}
})
} else if (this.state === 'rejected') {
queueMicrotask(() => {
try {
const value = onRejected(this.reason)
this.resolvePromise(newPromise, value, resolve, reject)
} catch (e) {
reject(e)
}
})
} else if (this.state === 'pending') {
// 初次进入then方法,将回调函数存入数组
this.resolveCallbacks.push(() => {
queueMicrotask(() => {
try {
const value = onResolve(this.data)
this.resolvePromise(newPromise, value, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
})
return newPromise
}
//5.实现Promise解决程序:为了符合Promise A+的规则
//传入的参数分别代表,最新的Promise,最新的值,最新的resolve,最新的reject
resolvePromise = (newPromise, value, resolve, reject) => {
//这里有三种情况:
//1.如果value和promise是同一个对象,则调用reject方法,其参数为new TypeError()
//2.如果value是对象或函数,且then是一个函数,新的reslove和reject函数作为参数。
// 如果value.then不是函数,就直接调用resolve方法处理Promise,参数为value
//3.如果以上两种状况都没有出现,调用resolve方法解决Promise,其参数为value
if (value === newPromise) {
// 循环调用报错
reject(new TypeError())
} else if ((value !== null && typeof value === 'object') || typeof value === 'function') {
if (typeof value.then === 'function') {
value.then(
(newValue) => {
resolvePromise(newPromise, newValue, resolve, reject)
},
(r) => {
reject(r)
}
)
} else {
resolve(value)
}
} else {
resolve(value)
}
}
//6.实现catch,catch函数只是没有给fulfilled状态预留参数位置的then方法
catch (onRejected) {
return this.then(undefined, onRejected);
}
//8.实现Promise.resolve()方法,如果是promise就直接返回value,否则就new一个promise,默认resolve传入value
static resolve (value) {
return value instanceof myPromise
? value
: new myPromise(resolve => resolve(value));
}
//9.实现Promise.reject()方法
static reject (reason) {
return new myPromise ((resolve, reject) => {
reject(reason);
});
}
//10.实现finally,相当于最后再执行then方法去调用静态方法reslove
finally (callback) {
return this.then (
data => {
return myPromise.resolve(callback().then (() => data));
},
err => {
return myPromise.resolve(callback()).then (() => {
throw err;
});
}
);
}
//11.实现all方法
static all(promises) {
return new myPromise((resolve, reject) => {
let times = 0; // 记录执行次数
let result = []; // 保存执行结果
function addData (key, value) {
// 记录结果
times++;
result[key] = value;
times === promises.length && resolve (result);
}
promises.forEach((element, index) => {
if (element instanceof myPromise) {
//promises数组中的元素是promise
element.then(
value => addData(index,data),
err => reject(err)
)
} else {
addData(index,element)
}
})
})
}
}
new myPromise((resolve,reject)=>{
console.log(1)
resolve(3)
console.log(2)
}).then(res=>{
console.log(res)
}).then(res=>{
debugger
console.log(res)
console.log(4)
})
6.async/await
async/await其实是Promise的语法糖,它是为优化then链而开发出来的。它能实现的效果都能用then来实现, 可以视await后面的方法为Promise的构造函数,之后的代码等同于then回调。
async的用法,它作为一个关键字放到函数前面,表示函数是一个异步函数,async函数返回的是一个promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
await 后面应该写一个Promise对象,如果不是Promise对象,那么会被转成一个立即resolve的Promise(Promise.resolve())。 await 命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在 try...catch 代码块中。
样例:
//函数声明
async function test(){}
//函数表达式
const res = async function(){}
//箭头函数
const res = async ()=>{}
主要应用场景:
样例1:需要等待接口一步步执行
async function getData(){
try{
let res1= await getA() //getA为Promise,是一个接口请求
if(res1){
let res2= await getB() //getB为Promise,是一个接口请求
return res2
}
}catch(e){
console.log(e)
}
}
样例2:只需要单个接口等待,其他接口可以异步
async function getData(){
getA().then(async (res)=>{
try{
//需要res2数据,才能继续执行
let res2= await getB() //getB为Promise,是一个接口请求
console.log(res)
}catch(e){
console.log(e)
}
})
}
7.闭包
在JavaScript高级程序设计(第3版)中是这样描述闭包:闭包是指有权访问另一个函数作用域中的变量的函数。
闭包作用:闭包既可以长久的保存变量又不会造成全局污染。
闭包缺点:占用更多内存;不容易被释放,不会随着函数的结束而自动销毁。
闭包用法:
1、定义外层函数,封装被保护的局部变量。
2、定义内层函数,执行对外部函数变量的操作。
3、外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。
function outTest(){
let a = 1
return function insideTest(){
a++
return a
}
}
outTest()()//outTest()的执行结果返回的是insideTest函数,我们需要再加一个()执行insideTest函数
//换个调用方式,结果就不一样了
var func = outTest()
outTest() //第一次结果都是2相同,而这种调用方式每次+1,而上面的方式始终不变,这是因为闭包中的变量没有被释放的原因。
8.递归
简单来说,所谓的递归函数就是在函数体内调用n次本函数,直到达到某个条件从而停止。
递归应用的场景需要具备以下三种要素:
1、存在递归终止条件:递归出口
2、一个问题可以分解为更小的问题用同样的方法解决:递归表达式(规律)
3、分解后的子问题求解方式一样,不同的是数据规模变小
//斐波那切数列:自身等于前两项之和,即n = (n-1) +(n-2)
//1、1、2、3、5、8、13
function fibonacci(n){
if(n<=2){
return 1
}
return fibonacci(n-1)+fibonacci(n-2)
}
console.log(fibonacci(6))
9.高阶函数
高阶函数是指至少满足下列条件之一的函数:
1、函数可以作为参数被传递
2、函数可以作为返回值输出。
所以一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
最常用的高阶函数,即我们日常接口所用的回调或者是对数据的使用,比如map、reduce、filter、sort 或如下:
//假设getData是一个接口请求
getData().then(res=>{
console.log(res)
})
getData().then(function(res){
console.log(res)
})
const arr = [1,2,3]
const arr2 = arr.map(item=>item*2)
const arr3 = arr.map(function(item){
return item+10
})
最后
走过路过,不要错过,点赞、收藏、评论三连~