CruxF / Blog

个人博客,记载学习的点点滴滴,怕什么技术无穷,进一寸有一寸的欢喜:sparkles:
63 stars 19 forks source link

前端工程师面试指南——ES6篇 #24

Closed CruxF closed 6 years ago

CruxF commented 6 years ago

ES6模块和CommonJS模块不同的地方

image

箭头函数需要注意的地方

当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。

下面来看一道面试题,重点说明下第一个知识点:

class Animal {
  constructor() {
    this.type = "animal";
  }
  say(val) {
    setTimeout(function () {
      console.log(this); //window
      console.log(this.type + " says " + val);
    }, 1000)
  }
}
var animal = new Animal();
animal.say("hi"); //undefined says hi

【拓展】 《JavaScript高级程序设计》第二版中,写到:“超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined”。也就是说在非严格模式下,setTimeout中所执行函数中的this,永远指向window!!

我们再来看看箭头函数(=>)的情况:

class Animal {
  constructor() {
    this.type = "animal";
  }
  say(val) {
    setTimeout(() => {
      console.log(this); //Animal
      console.log(this.type + ' says ' + val);
    }, 1000)
  }
}
var animal = new Animal();
animal.say("hi"); //animal says hi

【特点】

let和const

let是更完美的var,不是全局变量,具有块级函数作用域,大多数情况不会发生变量提升。const定义常量值,不能够重新赋值,如果值是一个对象,可以改变对象里边的属性值。

下面是var带来的不合理场景

var arr = [];
for (var i = 0; i < 10; i++) {
  arr[i] = function () {
    console.log(i);
  }
}
arr[5]() //10,a[5]输出f(){console.log(i);},后面加个括号代表执行f()

在上述代码中,变量i是var声明的,在全局范围类都有效,所以用来计数的循环变量泄露为全局变量。所以每一次循环,新的i值都会覆盖旧值,导致最后输出都是10。

而如果对循环使用let语句的情况,那么每次迭代都是为x创建新的绑定代码如下:

var arr = [];
for (let i = 0; i < 10; i++) {
  arr[i] = function () {
    console.log(i);
  }
}
arr[5]() //5,a[5]输出f(){console.log(i);},后面加个括号代表执行f()

【拓展】 当然,除了这种方式让数组找中的各个元素分别是不同的函数,我们还可以采用ES5中的闭包和立即函数两种方法。

1、采用闭包

function showNum(i) {
  return function () {
    console.log(i)
  }
}
var a = []
for (var i = 0; i < 5; i++) {
  a[i] = showNum(i)(); //循环输出1,2,3,4
}

2、采用立即执行函数

var a = []
for (var i = 0; i < 5; i++) {
  a[i] = (function (i) {
    return function () {
      console.log(i)
    }
  })(i)
}
a[2](); //2

【面试】 把以下代码使用两种方法,依次输出0-9

var funcs = []
for (var i = 0; i < 10; i++) {
  funcs.push(function () {
    console.log(i)
  })
}
funcs.forEach(function (func) {
  func(); //输出十个10
})

方法一:使用立即执行函数

var funcs = []
for (var i = 0; i < 10; i++) {
  funcs.push((function (value) {
    return function () {
      console.log(value)
    }
  }(i)))
}
funcs.forEach(function (func) {
  func(); //依次输出0-9
})

方法二:使用闭包

function show(i) {
  return function () {
    console.log(i)
  }
}
var funcs = []
for (var i = 0; i < 10; i++) {
  funcs.push(show(i))
}
funcs.forEach(function (func) {
  func(); //0 1 2 3 4 5 6 7 8 9
})

方法三:使用let

var funcs = []
for (let i = 0; i < 10; i++) {
  funcs.push(function () {
    console.log(i)
  })
}
funcs.forEach(function (func) {
  func(); //依次输出0-9
})

其他知识点:forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。戳这里查看参考文章

Set数据结构

es6方法,Set本身是一个构造函数,它类似于数组,但是成员值都是唯一的。

const set = new Set([1,2,3,4,4])
console.log([...set] )// [1,2,3,4]
console.log(Array.from(new Set([2,3,3,5,6]))); //[2,3,5,6]

Class的讲解

class语法相对原型、构造函数、继承更接近传统语法,它的写法能够让对象原型的写法更加清晰、面向对象编程的语法更加通俗,下面是class的具体用法。

class Animal {
  constructor() {
    this.type = 'animal'
  }
  says(say) {
    console.log(this.type + 'says' + say)
  }
}
let animal = new Animal()
animal.says('hello') // animal says hello

class Cat extends Animal {
  constructor() {
    super()
    this.type = 'cat'
  }
}
let cat = new Cat()
cat.says('hello') // cat says hell

可以看出在使用extend的时候结构输出是cat says hello 而不是animal says hello。说明contructor内部定义的方法和属性是实例对象自己的,不能通过extends 进行继承。在class cat中出现了super(),这是什么呢?因为在ES6中,子类的构造函数必须含有super函数,super表示的是调用父类的构造函数,虽然是父类的构造函数,但是this指向的却是cat。更详细的参考文章

模板字符串

就是这种形式${varible},在以往的时候我们在连接字符串和变量的时候需要使用这种方式'string' + varible + 'string'但是有了模版语言后我们可以使用string${varible}string这种进行连接。基本用途有如下:

1、基本的字符串格式化,将表达式嵌入字符串中进行拼接,用${}来界定。

//es5 
var name = 'lux';
console.log('hello' + name);
//es6
const name = 'lux';
console.log(`hello ${name}`); //hello lux

2、在ES5时我们通过反斜杠()来做多行字符串或者字符串一行行拼接,ES6反引号(``)直接搞定。

//ES5
var template = "hello \
world";
console.log(template); //hello world

//ES6
const template = `hello
world`;
console.log(template); //hello 空行 world

【拓展】 字符串的其他方法

// 1.includes:判断是否包含然后直接返回布尔值
let str = 'hahay'
console.log(str.includes('y')) // true

// 2.repeat: 获取字符串重复n次
let s = 'he'
console.log(s.repeat(3)) // 'hehehe'

重点“人物”:Promise!

【面试套路1】 手写一个promise

var promise = new Promise((resolve, reject) => {
  if (操作成功) {
    resolve(value)
  } else {
    reject(error)
  }
})
promise.then(function (value) {
  // success
}, function (value) {
  // failure
})

【面试套路2】 怎么解决回调函数里面回调另一个函数,另一个函数的参数需要依赖这个回调函数。需要被解决的代码如下:

$http.get(url).success(function (res) {
  if (success != undefined) {
    success(res);
  }
}).error(function (res) {
  if (error != undefined) {
    error(res);
  }
});

function success(data) {
  if( data.id != 0) {
    var url = "getdata/data?id=" + data.id + "";
    $http.get(url).success(function (res) {
      showData(res);
    }).error(function (res) {
      if (error != undefined) {
        error(res);
      }
    });
  }
}

【面试套路3】 以下代码依次输出的内容是?

setTimeout(function () {
  console.log(1)
}, 0);
new Promise(function executor(resolve) {
  console.log(2);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve();
  }
  console.log(3);
}).then(function () {
  console.log(4);
});
console.log(5);

// 首先先碰到一个 setTimeout,于是会先设置一个定时,在定时结束后将传递这个函数放到任务队列
里面,因此开始肯定不会输出 1 。 
// 然后是一个 Promise,里面的函数是直接执行的,因此应该直接输出 2 3 。 
// 然后,Promise 的 then 应当会放到当前 tick 的最后,但是还是在当前 tick 中。 
// 因此,应当先输出 5,然后再输出 4 , 最后在到下一个 tick,就是 1 。

【面试套路4】 jQuery的ajax返回的是promise对象吗?

// jquery的ajax返回的是deferred对象,通过promise的resolve()方法将其转换为promise对象。

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

【面试套路5】 promise只有2个状态,成功和失败,怎么让一个函数无论成功还是失败都能被调用?

// 使用promise.all()

// Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

// Promise.all方法接受一个数组作为参数,数组里的元素都是Promise对象的实例,如果不是,就会先调
// 用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数
// 可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)

// 示例:
var p =Promise.all([p1,p2,p3]);
// p的状态由p1、p2、p3决定,分为两种情况。
// 当该数组里的所有Promise实例都进入Fulfilled状态:Promise.all**返回的实例才会变成Fulfilled状态。
// 并将Promise实例数组的所有返回值组成一个数组,传递给Promise.all返回实例的回调函数**。

// 当该数组里的某个Promise实例都进入Rejected状态:Promise.all返回的实例会立即变成Rejected状态。
并将第一个rejected的实例返回值传递给Promise.all返回实例的回调函数。

【面试套路6】 一、分析下列程序代码,得出运行结果,解释其原因

const promise = new Promise((resolve, reject) => {
  console.log(1)
  resolve()
  console.log(2)
})
promise.then(() => {
  console.log(3)
})
console.log(4)

运行结果:
1 2 4 3

原因:
Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。

二、分析下列程序代码,得出运行结果,解释其原因

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})

console.log('promise1', promise1)
console.log('promise2', promise2)

setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)

运行结果:
promise1 Promise { <pending> }
promise2 Promise { <pending> }
(node:50928) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: error!!!
(node:50928) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
promise1 Promise { 'success' }
promise2 Promise {
  <rejected> Error: error!!!
    at promise.then (...)
    at <anonymous> }

原因:
promise 有 3 种状态:pending(进行中)、fulfilled(已完成,又称为Resolved) 或 rejected(已失败)。状态改变只能是 pending->fulfilled 或者 pending->rejected,状态一旦改变则不能再变。上面 promise2 并不是 promise1,而是返回的一个新的 Promise 实例。

三、分析下列程序代码,得出运行结果,解释其原因

const promise = new Promise((resolve, reject) => {
  resolve('success1')
  reject('error')
  resolve('success2')
})

promise
  .then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })

运行结果:
then:success1
原因:
构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用,呼应代码二结论:promise 状态一旦改变则不能再变。

四、分析下列程序代码,得出运行结果,解释其原因

Promise.resolve(1)
  .then((res) => {
    console.log(res)
    return 2
  })
  .catch((err) => {
    return 3
  })
  .then((res) => {
    console.log(res)
  })

运行结果:
1  2
原因:
promise 可以链式调用。提起链式调用我们通常会想到通过 return this 实现,不过 Promise 并不是这样实现的。promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用。

五、分析下列程序代码,得出运行结果,解释其原因

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('once')
    resolve('success')
  }, 1000)
})

const start = Date.now()
promise.then((res) => {
  console.log(res, Date.now() - start)
})
promise.then((res) => {
  console.log(res, Date.now() - start)
})

运行结果:
once
success 1001
success 1001
原因:
promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。

六、分析下列程序代码,得出运行结果,解释其原因

Promise.resolve()
  .then(() => {
    return new Error('error!!!')
  })
  .then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })

运行结果
then: Error: error!!!
    at Promise.resolve.then (...)
    at ...

原因
.then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获,需要改成其中一种:
return Promise.reject(new Error('error!!!'))
throw new Error('error!!!')

因为返回任意一个非 promise 的值都会被包裹成 promise 对象,即 return new Error('error!!!') 等价于 return Promise.resolve(new Error('error!!!'))。

七、分析下列程序代码,得出运行结果,解释其原因

const promise = Promise.resolve()
  .then(() => {
    return promise
  })
promise.catch(console.error)

运行结果
TypeError: Chaining cycle detected for promise #<Promise>
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
    at Function.Module.runMain (module.js:667:11)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:607:3
原因
.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。

八、分析下列程序代码,得出运行结果,解释其原因

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

运行结果:
1
原因:
.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。

九、分析下列程序代码,得出运行结果,解释其原因

Promise.resolve()
  .then(function success (res) {
    throw new Error('error')
  }, function fail1 (e) {
    console.error('fail1: ', e)
  })
  .catch(function fail2 (e) {
    console.error('fail2: ', e)
  })

运行结果:
fail2: Error: error
    at success (...)
    at ...
原因:
.then 可以接收两个参数,第一个是处理成功的函数,第二个是处理错误的函数。.catch 是 .then 第二个参数的简便写法,但是它们用法上有一点需要注意:.then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的 .catch 可以捕获之前的错误。

十、分析下列程序代码,得出运行结果,解释其原因

process.nextTick(() => {
  console.log('nextTick')
})
Promise.resolve()
  .then(() => {
    console.log('then')
  })
setImmediate(() => {
  console.log('setImmediate')
})
console.log('end')

运行结果:
end
nextTick
then
setImmediate
原因:
process.nextTick 和 promise.then 都属于 microtask,而 setImmediate 属于 macrotask,在事件循环的 check 阶段执行。事件循环的每个阶段(macrotask)之间都会执行 microtask,事件循环的开始会先执行一次 microtask。