前言
失踪博主终于找回自己的账号密码了0V0,这次给大家带来的是Es6的一些的面试问题,不知道看过前面文章的小伙伴们有没有找到心仪的工作呢?
ES6添加了一系列新的语言特性,其中一些特性比其它更具有开创性以及更广的可用性。
列举常用的ES6方法:
- let、const
- 箭头函数
- 类的支持
- 字符串模块
- Promises
箭头函数需要注意哪些地方?
当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。
-
在使用=>定义函数的时候,this的指向是定义时所在的对象,而不是使用时所在的对象;
-
不能够用作构造函数,这就是说,不能够使用new命令,否则就会抛出一个错误。
-
不能够使用arguments对象;
-
不能使用yield命令
let、const、var
var声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的,由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明;
let是更完美的var,不是全局变量,具有块级函数作用域,大多数情况不会发生变量提升。
-
let声明的变量具有块级作用域
-
let生命的变量不能通过window.变量名访问
-
形如for(let x...)的循环是每次迭代都为x创建新的绑定
-
let约束了变量提升
-
let不允许在相同作用域内重复声明同一个变量名,var是允许的
const定义的常量值,不能够重新赋值,如果值是一个对象,可以改变对象里边的属性值。const变量声明的时候必须初始化
拓展:var方式定义的变量有什么样的bug?
- js没有块级作用域,在Js函数中的var声明,其作用域是函数体的全部
var
for(var i=0;i<10;i++){
var a = 'a';
}
console.log(a,i); //a 10
let
for(let i=0;i<10;i++){
let a = 'a';
}
console.log(a,i); //a is not defined
- 循环内变量过度共享
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000);
}//3个3
这里循环本身及三次timeout回调均共享唯一的变量i。当循环结束执行时,i的值为3.所以当第一个timeout执行时,调用的i当然也为3了。
Set数据结构
ES6中的Set方法本身是一个构造函数,它类似于数组,但是成员的值都是唯一的
拓展:数组去重的方法
ES6 set方法
var arr = new Set([1,2,2,3,4]);
console.log([...arr]); //(4) [1, 2, 3, 4]
以往去重方法
var arr = [1,1,2,2,3,4];
//创建一个空数组用于接收不重复内容的数组
var new_arr = [];
for(var i = 0;i<arr.length;i++){
if(new_arr.indexOf(arr[i])==-1){ //判断arr[i]在new_arr中是否存在相同的内容,不存在则push到数组中
new_arr.push(arr[i])
}
}
console.log(new_arr); //(4) [1, 2, 3, 4]
箭头函数this的指向。
在非箭头函数下,this指向调用其所在的函数对象,而且是离谁近就指向谁(此对于常规对象,原型链,getter&setter等都适用);
构造函数下,this与被创建的新对象绑定;DOM事件下,this指向触发事件的元素;内联事件分为两种情况,bind绑定,call&apply等方法改变this指向等。而有时this也会指向window;
所以ES6的箭头函数,修复了原有的this指向问题。
手写ES6 class继承。
//定义一个类
class Children{
constructor(skin,language){
this.skin = skin;
this.language = language;
}
say(){
console.log("I'm a Person")
}
}
class American extends Children{
constructor(skin,language){
super(skin,language)
}
aboutMe(){
console.log(this.skin+" "+this.language)
}
}
var m = new American("张三","中文");
m.say();
m.aboutMe();
1)子类没有constructor
子类American继承父类Person,子类没用定义constructor则默认添加一个,并且在constructor中调用super函数,相当于调用父类的构造函数。调用super函数是为了在子类中获取父类的this,调用之后this指向子类。也就是父类prototype.constructor.call(this)
2) 子类有constructor
子类必须在constructor方法中调用super方法,否则new实例时会报错。因为子类没有自己的this对象,而是继承父类的this对象。如果不调用super函数,子类就得不到this对象。super()作为父类的构造函数,只能出现在子类的constructor()中,但是super指向父类的原型对象,可以调用父类的属性和方法。
const foo = async() => {};
generator生成器函数:
Generator(生成器)是ES6标准引入的新数据类型,一个Generator看上去像是一个函数,但可以返回多次。Generator的声明方式类似一般的函数声明,只是多了个*号,并且一般可以在函数内看到yield关键字。
调用generator对象有两个方法,一是不断的调用generator对象的next()方法,第二个方法是直接用for。。。of循环迭代generator对象。
每调用一次next,则执行一次yield语句,并在该处暂停。return完成后退出生成器函数,后续如果还有yield操作就不再执行了。
<script>
function * showWords(){
yield "one";
yield "two";
return 'three'
}
var show = showWords();
console.log(show.next()); //{value: "one", done: false}
console.log(show.next()); //{value: "two", done: false}
console.log(show.next()); //{value: "three", done: true}
console.log(show.next()); //{value: "undefined", done: true}
</script>
yield和yield*
//yield
function * showWords(){
yield "one";
yield showNumber();
return 'three';
}
// yield *
function * showWords2(){
yield "one";
yield* showNumber(2,3);
return 'three';
}
function * showNumber(a,b){
yield a+b;
yield a*b;
}
var show = showWords();
console.log(show.next()); //{value: "one", done: false}
console.log(show.next()); //{value: "showNumber", done: false}
console.log(show.next()); //{value: "three", done: true}
var show2 = showWords2();
console.log(show2.next()); //{value: "one", done: false}
console.log(show2.next()); //{value: 5, done: false}
console.log(show2.next()); //{value: 6, done: true}
console.log(show2.next()); //{value: "three", done: true}
什么是async/await及其如何工作?
async/await是JS中编写异步或非阻塞代码的新方式。它建立在Promises之上, 让异步代码的可读性和简洁度都更高。async关键声明函数会隐式返回一个Promise。await关键字只能在async function中使用,在仁和非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回
async函数的基本用法:
asyn函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。由于async函数返回的是Promise对象,可以作为await命令的参数。
ES6 async函数有多种使用形式:
//函数声明
async function foo(){}
//函数表达式
const foo = async function(){}
//对象的方法
let obj = {async foo()}
obj.foo().then(...)
//class方法
class Storage{
constructor(){
this.cachePromise = cache.open("avatars")
}
async getAvatar(name){
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`)
}
}
//箭头函数
async与generator的区别?
async函数是Generator函数的语法糖,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await。
async函数对Generator函数的改进,体现在以下四点:
- 内置执行器
- 更好的语义:async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果
- 更广的适用性:async函数的awai他命令后面,可以是Promise对象和原始类型的值(数字、字符串和布尔值,但这时等同于同步操作)
- 返回值是Promise:async函数的返回值是Peomise对象,这比Generator函数的返回值是Iterator对象方便对了,可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作包装成的一个Promise对象,而await命令就是内部then命令的语法糖
简单实现async/await中的async函数
async函数的实现原理,就是将Generator函数和自动执行器,包装在一个函数里
function spawn(genF){
return new Promise(function(resolve,reject){
const gen = genF();
function step(nextF){
let next;
//ry/catch/finally 语句用于处理代码中可能出现的错误信息。
try {
next = nextF()
}catch(e){
return reject(e);
}
if(next .done){
return resolve(next.value);
}
Promise.resolve(next.value).then(
function(v){
step(function(){
return gen.next(v)
});
},
function(v){
step(function(){
return gen.throw(e)
});
}
)
}
step(function(){
return gen.next(undefined);
});
})
}
有用过promise吗?请写出下列代码的执行结果,并写出你的理解思路:
setTimeout(()=>{
console.log(1);
}, 0);
new Promise((resolve)=>{
console.log(2);
for(var i = 1; i < 200; i++){
i = 198 && resolve();
}
console.log(3);
}).then(()=>{
console.log(4);
});console.log(5);
//想理解promise就得知道js的执行顺序,这里顺便讲一下同步异步
首先要讲一下,js是单线程执行,那么代码的执行就有先后;
有先后,那就要有规则(排队),不然就乱套了,那么如何分先后呢?大体分两种:同步、异步;
同步很好理解,就不用多说了(我就是老大,你们都要给我让路);
异步(定时器[setTimeout ,setInterval]、事件、ajax、promise等),说到异步又要细分宏任务、微任务两种机制,
宏任务:js异步执行过程中遇到宏任务,就先执行宏任务,将宏任务加入执行的队列(event queue),然后再去执行微任务;
微任务:js异步执行过程中遇到微任务,也会将任务加入执行的队列(event queue),但是注意这两个queue身份是不一样的,不是你先进来,就你先出去的(就像宫里的皇上选妃侍寝一样,不是你先进宫(或先来排队)就先宠幸的 ),真执行的时候是先微任务里拿对应回调函数,然后才轮到宏任务的队列回调执行的;
理解了这个顺序,那上面的结果也就不难懂了。
Object.is()与原来的比较操作符===,==的区别?
==相等运算符,比较时会自动进行数据类型转换
===严格相等运算符,比较时不进行隐式类型转换
Object.is同值相等算法,在===基础上对0和NaN特别处理
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
介绍一下Set、Map、WeakSet和WeakMap的区别?
Set: 成员不能重复
只有键值,没有键名
可以遍历,方法有add,delete,has
WeakSet:成员都是对象
成员都是弱引用,随时可以消失,可以用来保存DOM节点,不容易造成内存泄漏
不能遍历,方法有add,delete ,has
Map: 本质上是键值对的集合,类似集合
可以遍历,方法很多,可以跟各种数据格式转换
WeakMap:只接受对象作为键名(null除外),不接受其他类型的值作为键名
键名所指向的对象,不计入垃圾回收机制
不能遍历,方法有get、set、has、delete
ES5的继承和ES6的继承有什么区别?
ES5的继承实质上是先创建子类的实例对象, 然后再将父类的方法添加到this上(Parent.apply(this))
ES6继承机智完全不同, 实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法), 然后再用子类的构造函数修改this
具体的: ES6通过class关键字定义类, 里面有构造方法, 类之间通过extends关键字实现继承。子类必须在constructor方法中哄调用super方法,否则新建实例报错。 因为子类没有自己的this对象,而是继承了父类的this对象, 然后对其进行加工。如果不调用super方法, 子类得不到this对象
PS:super关键字指代父类的实例,即父类的this对象。在子类构造函数中, 调用super后,才可使用this关键字,否则报错
ES5的继承:
- 原型链继承
- 构造函数继承
- 组合继承
- 寄生组合继承
ES6的extends继承
super关键字的使用, 新增的静态字段使用, 支持对原生构造函数的继承, 对object继承的差异
ES6 class 的new实例和ES5的new实例有什么区别?
相同点:
1、 实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
2、类的所有实例共享一个原型对象
不同点:
1、 ES6用class进行实例和普通构造函数进行实例
2、lass不存在变量提升
3、ES6为new命令引入了一个new.target属性(在狗仔函数中) 但会new命令作用域的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined
ES6新特性详细介绍说明:
变量声明:const和let:
ES6推荐使用let声明局部变量,相比之前的var(无论声明在何处,都会被视为声明在函数的最顶部),而let不会将声明变量提前;
let表示声明变量,而const表示声明常量,两者都为块级作用域;const声明的变量都会被认为是常量,意思就是它的值被设置完成后就不能再修改了。
注意:let关键词声明的变量不具备变量提升的特性
const和let声明只在最高进的一个块中(花括号内)有效
const在声明时必须被赋值
箭头函数:
箭头函数是函数的一种简写形式,使用括号包裹参数,跟随一个=>,紧接着是函数体
箭头函数最直观的三个特点:
不需要function关键字来创造
省略return关键字
修复了this指向
类和继承:
class和extend是一种语法糖,也是基于原型继承实现的
class和super calls,实例化,静态方法和构造函数
<script>
//class声明类 内部直接是属性和方法 不用,分隔。 constructor
class Person{
constructor(name, age){
this.name = name;//实例属性
this.age = age;
console.log(name,age)
}
sayhi(name){
//使用ES6新特性字符串模块,注意外层包裹符号是``反引号来创建字符串,内部变量使用${}
console.log(`this name is ${this.name}`);
//以往的写法
console.log('my age is '+this.age)
}
}
class Programmer extends Person{
constructor(name,age){
//直接调用父类结构器进行初始化
super(name,age)
}
program(){
console.log("I'm coding...")
}
}
var anim = new Person("张三",18);
anim.sayhi();
var wayou = new Programmer("李四",20);
wayou.sayhi();
wayou.program();
</script>
字符串模板:
ES6中允许使用反引号` 来创建字符串,此方法创建的字符串里面可以包含由${ }包裹的变量
<script>
//产生一个随机数
var num = Math.random();
//将这个数字输出到console
console.log(`输出随机数${num}`);
</script>
增强的对象字面量:
对象字面量被增强了,写法更加简洁与玲姐,同时在定义对象的时候能够做的事情更多了。具体表现在:
-
可以在对象子里面量里面定义原型
-
定义方法可以不用function关键词
-
直接调用父类方法
<script>
var human = {
breathe(){
console.log('breathing...');
}
};
var worker = {
__proto__:human, //设置此对象的原型为human,想党羽继承human
company:'freelancer',
work(){
console.log("working..")
}
}
human.breathe();
//调用继承来的breathe方法
worker.breathe();
</script>
解构:
</script>
<script>
var [x,y] = getVal(), //函数返回值解析
[name, ,age] = ["wayou","male","secrect"]; //数组解析
function getVal(){
return [1,2];
}
console.log(`x:${x},y:${y}`); //x:1,y:2
console.log(`name:${name},age:${age}`); //name:wayou,age:secrect
</script>
参数默认值,不定参数,拓展参数:
默认参数值:
可以在定义函数的时候指定参数的默认值,而不用像以前那样通过逻辑或操作符来达到目的了。
<script>
//传统方式设置默认方式
function sayHello(name){
var name = name||'dude';
console.log("Hello "+name);
}
sayHello(); //Hello dude
sayHello("wayou"); //Hello wayou
//ES6的默认参数
function sayHello2(name = "dude"){
console.log("Hello "+name)
}
sayHello2(); //Hello dude
sayHello2("wayou"); //Hello wayou
</script>
不定参数(拓展符):
不定参数是在函数中使用命名参数同时接收不定数量的未命名参数。这只是一种语法糖,在以前的JavaScript代码中我们可以通过arguments变量来达到这一目的。不定参数的格式是三个句点后跟代表所有不定参数的变量名。比如下面这个例子中,…x代表了所有传入add函数的参数。
<script>
//将所有参数想加的函数
function add(...x){
return x.reduce((m,n)=>m+n);
}
//传递任意个数的参数
console.log(add(1,2,3)); //输出:6
console.log(add(1,2,3,4,5)); //输出:15
</script>
reduce()方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
reduce()可以作为一个高阶函数,用于函数的compose
注意:reduce()对于空数组是不会执行回调函数的
拓展符:将一个数组转为用逗号分隔的参数序列。(若数组为空不产生任何效果)
<script>
var x = [1,2,3,4,5,6];
console.log(x); //(6) [1, 2, 3, 4, 5, 6]
console.log(...x); //1 2 3 4 5 6
</script>
拓展参数:
拓展参数则是另一种形式的语法糖,它允许传递数组或类数组直接作为函数的参数而不用通过apply。
<script>
var people = ['wayou','john','sherlock'];
//sayHello函数来接收三个单独的参数
function sayHello(people1,people2,people3){
console.log(`Hello ${people1},${people2},${people3}`)
}
//以前的方式,如果需要传递数组当参数,我们需要使用函数apply方法
sayHello.apply(null,people); //Hello wayou,john,sherlock
sayHello(...people); //Hello wayou,john,sherlock
</script>
for of值遍历:
for in循环用于遍历数组,类数组或对象,ES6中新引入的for of循环功能相似,不同的是每次循环他提供的不是序号而是值
<script>
var someArray = ['a','b','c'];
for(v of someArray){
console.log(v); //a,b,c
}
</script>
iterator/generator:
iterator:它是这么一个对象,拥有一个next方法,这个方法返回一个对象{done,value},这个对象包含两个属性,一个布尔类型的done和包含任意值的value。
iterable:这是这么一个对象,拥有一个obj[@@iterator]方法,这个方法返回一个iterator
generator:它是一个特殊的iterator。反的next方法可以接收一个参数并且返回值取决于它的构造函数(generator function)。generator同时拥有一个throw方法。
generator番薯:即generator的构造函数。此函数内可以使用yield关键字,在yield出现的地方可以通过generator的next或throw方法向外界传递值。generator函数是通过function*来声明的。
yield关键字:它可以暂停函数的执行,随后可以再进入函数继续执行
具体详情:https://blog.domenic.me/es6-iterators-generators-and-iterables/
模块:
在ES6标准中,Javascript原生支持module了。这种将JS代码分割成不同功能的小块进行模块化的概念是在一些三方规范中流行起来的,比如CommonJS和AMD模式。
将不同功能的代码分别写在不同文件中,各模块只需导出公共接口部分,然后通过模块的导入方式可以在其他地方使用。
<script>
//单独的js文件,如:point.js
module "point" {
export class Point {
constructor (x,y){
publice x = x;
publice y = y;
}
}
}
//在需要引用模块的js文件内
//声明引用的模块
module point from './point.js';
//这里可以看出,尽管声明了引用的模块,还是可以通过指定需要的部分进行导入
import Point from "point"
var origin = new Ponit(0,0);
console.log(origin)
</script>
Map、Set和WeakMap、WeakSet
这些是新加的集合类型,提供了更加方便的获取属性值的方法,不用像以前一样用hasOwnProperty来检查某个属性是属于原型链上的还是当前对象的。同时,在进行属性值添加与获取时有专门的get、set方法。
//Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello")===true;
//Maps
var m = new Map();
m.set("hello",42);
m.set(s,34);
m.get(s) === 34;
我们会把对象作为一个对象的键来存放属性值,普通集合类型比如简单对象会阻止垃圾回收器对这些作为属性键存在的对象回收,偶造成内存泄露的危险。而weakMap,weakSet则更加安全些,这些作为属性键的对象如果没有别的变量在引用它们,则会被回收释放掉,具体还看下面的例子。
//weak Maps
var wm = new WeakMap();
wm.set(s,{eatra:42});
wm.size === undefined;
//weak Sets
var ws = new WeakSet();
ws.add({data:42}); //因为添加到ws的这个临时对象没有其他变量引用它,所以ws不会保存它的值,也就是说这次添加其实没有意思
Proxies
proxy可以监听对象身上发生了什么事情,并在这些事情发生后执行一些相应的操作。一下子让我们对一个对象有了很强的跟踪能力,同时咋数据绑定方面也很有用处。
<script>
//定义被侦听的目标对象
var engineer = {name:"Join",salary:50};
//定义处理程序
var interceptor = {
set:function(receiver,property,value){
console.log(property,"is changed to",value);
receiver[property] = value;
}
}
//创建代理以进行侦听
engineer = new Proxy(engineer,interceptor);
//做一些改动来触发代理
engineer.salary = 60; //控制台输出:salary is changed to 60
</script>
上面代码,我们已经添加了注释,这里进一步解释。对于处理程序,是在被侦听的对象身上发生了相应事件之后,处理程序里面的方法会被调用,上面例子中我们设置了set的处理函数,表明。如果我们侦听对象的属性被更改,也就是被set了,那这个处理程序就会被调用,同时通过参数能够的值是哪个属性被更改,更改为什么值
symbols
symbols是一种基本类型,像数字、字符串还有布尔一样。它不是一个对象。symbols通过调用symbol函数产生,接收一个可选的名字参数,该函数返回的symbol是唯一的。之后就可以用这个返回值作为对象的键了。symbol还可以用来创建私有属性,外部无法直接访问偶symbol作为键的属性值。
<script>
var MyClass = (function() {
// module scoped symbol
var key = Symbol("key");
function MyClass(privateData) {
this[key] = privateData;
}
MyClass.prototype = {
doStuff: function() {
this[key]
}
};
return MyClass;
})();
var c = new MyClass("hello")
console.log(c["key"] === undefined); //true,无法访问该属性,因为是私有的
</script>
Math、Number、String、Object的新API
对Math、Number、String还有Object等添加了许多新的API。
Promises
Promise是处理异步操作的一种模式,之前在很多三方库中有实现,比如JQuery的deferred对象。当你发起一个异步请求,并绑定了.when()/.done()等事件处理程序时,其实就是在应用promise模式。
<script>
//创建Promise
var promise = new Promise(function(resolve,reject){
//进行一些异步或耗时操作
if(/*如果成功*/){
resolve("stuff worked")
}else{
reject(Error("It broke"));
}
});
//绑定处理程序
promise.then(function(result){
//promise成功的话会执行这里
console.log(result); //"stuff worked"`在这里插入代码片`
},function(err){
//promise处理失败会执行这里
console.log(err); //Error:"It broke"
});
</script>