ES6 基础知识
ES6 面试题
引言
- ECMAScript5,即 ES5 ,是ECMAScript 的第五次修订,于2009年完成标准化
- ECMAScript6,即ES6,是ECMAScript的第六次修订,于2015年完成,也称ES2015
一、let, var, const
三者的比较
1、var 会导致的问题:
-
var 命令会导致变量提升现象,即变量可以在声明之前使用,值为undefined,变量会被提升到作用域的顶端。
var a = 1; function test1(){ console.log(a);//undefined ,因为这里存在变量提升,这里 a 没有先声明就使用了,会将 a 的声明提到顶部 var a = 2; console.log(a);// 重新声明了 a 又给赋值,所以这里为 2 } test1(); //变量提升,如上面代码等价于 var a = 1; function test2(){ var a;//变量提升 console.log(a); var a = 2; console.log(a); }
-
内层变量可能覆盖外层变量
如上述变量提升案例,实际上内层由于先使用 a 再定义 a,导致变量提升,那么内部的变量就把外部的变量a 就给污染了。
-
用来计数的循环变量泄露为全局变量
var s = [1, 2, 3, 4, 5]; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i) //5 //上述代码,由于使用 var 声明变量,导致 i 泄露为全局变量。
2、let
-
声明的全局变量不会挂在在顶层对象下(即let 声明的全局变量不会挂在window 或 global下面)
//考察 let let a = 1; var b = 2; console.log(window.a) // undefiend, 因为 let 声明的全局变量不会挂在在全局对象下 console.log(window.b)// 2 ,使用 var 声明的全局变量,会挂在全局对象下
-
所有的变量一定要在声明后使用,否则会报错。
console.log(a) //Uncaught ReferenceError: a is not defined let a = 1;
-
暂时性死区,只要块级作用域内存在 let 命令,它所声明的变量就“绑定”( binding )这个区域,不再受外部的影响,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。
3、const
- 同 let一样,声明的全局变量不会挂在顶层对象下面
- const 声明后必须马上赋值,否则会报错
- const 作为简单类型,一旦声明就不能更改,复杂类型(对象、数组等)指针的地址不能修改,内部数据可更改。
const obj1 = { a: 1, b: 2 } obj1 = {};//报错,Uncaught SyntaxError: Identifier 'obj1' has already been declared obj1.b = 3;// 不报错,因为对象里面的值是可以改的
- const 声明也不存在提升现象,同样存在暂时性死区,只能在声明后使用。
二、箭头函数
1、箭头函数this的指向问题,箭头函数与普通函数的区别,你说箭头函数没有自己的this,那么 (()=>{}).bind(this)
可以吗?
-
箭头函数提供了一种更简洁的函数书写方式,不可以作为构造函数,也就是不能使用new命令,否则会报错
var f = v => v // 等价于 var f = function (a) { return a; } f(1) //1
-
当箭头函数要返回对象的时候,为了区分与代码块,要用
()
将对象包裹起来//报错,Uncaught SyntaxError: Unexpected token ':' var f = (id, name) => { id: id, name:name } // 不报错,返回对象时,要使用 () 括起来,和 {} 做区分 var f = (id, name) => ({ id: id, name: name })
2、箭头函数六没有
-
1、没有this
箭头函数没有this,所以需要通过查找作用域链来确定this的值。这就意味着如果箭头函数被非箭头函数包含,this绑定的就是最近一层非箭头函数的this。
因为箭头函数没有this,所以也不能用call()、apply()、bind()
这些方法改变this的指向。var value = 1; var result = (() => this.value).bind({value: 2})(); console.log(result); // 1,上述方法将 this绑定到新对象上,不会改变结果
-
2、没有arguments
箭头函数没有自己的arguments对象,因为箭头函数可以访问外围函数的,通过命名参数或者 rest 参数的形式访问参数。
-
3、没有new关键字调用
箭头函数不能被用作构造函数,如果通过new方式调用,会报错
var Foo = () => {}; var foo = new Foo(); // TypeError: Foo is not a constructor
-
4、没有
new.target
因为不能使用 new调用,所以也没有
new.target
值 -
5、没有原型
由于不能使用 new 调用箭头函数,所以也没有构造原型的需求,于是箭头函数不存在 prototype 这个属性
-
6、没有super
连原型都没有,自然也不能通过 super来访问原型的属性。
三、对象属性缩写
-
1、对象的方法缩写
//1、对象的方法缩写 const o = { method() { return "hello"; } } //等同于 const o = { method: function () { return "hello"; } } let birth = "1999/09/08"; const Person = { name: "张三", birth,//等同于 birth:birth //等同于 hello:function (){ ...} hello() { console.log('我的名字是', this.name); } }
-
2、对象的属性缩写
const a = 'a'; const b = { a }; //等同于 const b = { a: a }; function f(x, y) { return { x, y } }; //等同于 function f(x, y) { return { x: x, y: y } }
四、Object.Keys()、Object.Values()、Object.Assign() 方法
1、Object.Keys()
参数:要求返回其没见自身属性的对象。
返回值:一个表示给定对象的所有可枚举属性的字符串数组:
-
处理对象:返回可枚举的属性数组。
let person = {name:"张三", age:25, address:"深圳", getName:function(){}} Object.keys(person) // ["name", "age", "address", "getName"]
-
处理数组,返回索引值数组
let str = "saasd字符串" Object.keys(str) // ["0", "1", "2", "3", "4", "5", "6", "7"]// 返回的是一个索引值数组
-
常用技巧:
let person = {name:"张三",age:25,address:"深圳",getName:function(){}} Object.keys(person).map((key)=>{ person[key] // 获取到属性对应的值,做一些处理 })
2、Object.values()
与 Object.keys()
刚好相反,它是把一个对象的值转换为数组。
let person = {name:"张三", age:25, address:"深圳", getName:function(){}}
Object.keys(person).map((key)=>{
person[key] // 获取到属性对应的值,做一些处理
})
3、Object.Assign() 方法,用于合并对象、或者复制对象
//Object.Assign(),可以用来合并对象,或者复制对象,简写可以使用操作运算符...
let json = { a: 3, b: 4 };
let json2 = { ...json } // 这种写法等价于 Object.Assign({},json)
//注意合并后的对象和之前的对象不相等
console.log(json2 == json) //false
console.log(Object.assign({},json) == json)//false
console.log(Object.assign(json) === json)// true,因为并没有合并成新对象
//相同的键,后者会覆盖前者
let json3 = { a: 4, c: 5 }
let json4 = Object.assign(json, json3); //json4 返回 {a:4,b:4,c:5}
五、import /export/ ,require/export 区别
提问: ES6 中 import 和 export 和 CommonJS 中的 require/exports 中的区别是什么?
-
1、CommonJS模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值
ES6模块输出的是值的引用,JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。 等到脚本真正执行时,在根据引用到被加载的那个模块里面去取值。 ES6模块是动态引用,并且不会缓存运行结果,而是动态的去被加载的模块取值,模块里面的变量绑定其所在的模块。
-
2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(即ES6 可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高)
-
3、ES6 模块是通用的,同一个模块不用修改,就可以用在浏览器
-
4、require / exports 是 CommonJS 在 Node 中实现的,import / export 是 ES6 的模块,对ES6 只要使用 babel 转换就可以了。
-
5、ES6 模块的设计思想,是尽量静态化,使得编译时就能确定模块的依赖关系以及输入和输出的变量,export命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
六、解构赋值
注意: 解构的核心思想,是左右两边,结构要保持一致
//1、初始化赋值,利用数组解构
let [a, b, c] = [1, 2, 3]
//2、对象解构
let { name, age, arr, f } = {
name: "jerry",
age: 32,
arr: [{ name: 'liu' }, { name: 'zhao' }],
f: function () { console.log("hello world") }
}
//3、数组右侧缺省解构
let [a, b, c] = ['a', 'b']// c 会结构不到,为undefined
//4、数组解构,有默认参数
let [a, b, c = 'no-data'] = ['a', 'b']// 解构不到的,会按默认值填充
console.log(a, b, c)//a b no-data
//5、a,b 交换
let a = 1;
let b = 2;
let [a, b] = [b, a]// 使用解构,快速交换两个变量的值
//4、解构传入undefined 等,不会被解构为具体值
function fn({ a, b = '默认值' }) {
console.log(a)//1
console.log(b)//默认值
}
fn({ a: 1, b: undefined }) // undefined 不会被解构出来
fn({ a: 1, b: null }) // null 会被解构出来
fn({ a: 1, b: '' }) //字符串空会被解构出来
七、 Map 和 Set
1、Set 类似数组,但里面不能有重复
- Set 对象允许你存储任何类型的唯一值,无论原始数据是值还是对象引用
//1、数组去重
let arr = ['a','b','a'];
let newArr = new Set(arr);
//1、Set 用法:
let setArr = new Set(['a','b']);
setArr.add('a');//往 Set 里添加一项
setArr.delete('b'); //删除Set 中的一项
setArr.has('a');// true
setArr.size// 1, 返回个数 ,注意这个是个属性,不是一个函数
setArr.clear();//清空Set
- Set 中的特殊值:(Set 对象存储的值总是唯一的,所以要判断两个值是否恒等,有几个值需要特殊对待)
- 1) +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复。
- 2) undefined 与 undefined 是恒等的,所以不重复。
- 3) NaN 与 NaN 是不恒等的,但是Set 中只能存一个,所以不重复
2、Map 是ES6 中新增的数据结构,Map 类似于对象,但普通对象的key 必须是字符串或数字,而Map 的key 可以是任意数据类型。
-
Map 常规操作
const map = new Map(); const obj = { p: "hello world" }; map.set(obj, 'OK') // 设置一个值,key 可以使任意类型 map.get(obj)// OK map.has(obj) //true map.delete(obj)//true map.has(obj)// false map.clear();//清空所有
-
Map 遍历操作
const map1 = new Map(); map1.set('aaa', 100); map1.set('bbb', 200); //1、keys() // 返回键名的遍历器 for (let key of map1.keys()) { console.log(key); } // "aaa" // "bbb" //2、values() 返回值的遍历器 for (let value of map1.values()) { console.log(value); } // 100 // 200 //3、entries() 返回所有成员的遍历器,item 中是一个数组,即["aaa",100] for (let item of map1.entries()) { console.log(item[0], item[1]); } //aaa 100 //bbb 200 //4、forEach 遍历所有成员 map1.forEach((value, key) => { console.log(value, key); }) // 100 "aaa" // 200 "bbb"
3、数组中的map 方法,这点要和Map() 这种数据类型做区分
-
定义: 用于对数组中的每个元素进行处理,得到新的数组
-
特点:不改变原有的数据的结构和数据
const arr = [1, 2, 3, 4]; //返回上述数组的每一项值求平方的结果 const newArr = arr.map(item => item * item);//[1, 4, 9, 16]
八、JS数据类型以及Symbol 类型
1、关于JS 的基本类型:
-
JS ES5的 6 种数据类型
即:
String、Number、 Boolean、 undefined、 object、 null
ES6 新增了一种数据类型
Symbol
,谷歌67版本中还出现了一种bigInt
。是指 安全存储、操作大整数。所以目前一共有8种即:
String、Number、 Boolean、 undefined、 object、 null 、Symbol、bigInt
-
基本类型 和 引用类型
基本类型:除object外,即
String、 Number、boolean、null、undefined
引用类型:即object,里面包含function、Array、Date
2、关于Symbol 类型:
说明: ES6 引入了一种新的原始数据类型 Symbol
,表示独一无二的值,凡是属性名属于Symbol
类型的,就是独一无二的,可以保证不会与其他属性名产生冲突。
-
基本用法:
let s = Symbol() typeof s //"symbol" //注意:Symbol 函数前面不能使用new 命令否则会报错,它是一种类似字符串的数据类型
-
独一无二性(Symbol 函数的参数只是表示对当前Symbol 值得描述(独一无二性),因此相同参数的Symbol 函数返回值是不相等的。 )
let s1 = Symbol(); let s2 = Symbol(); s1 === s2 // false let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false,即使字符串相等,但是Symbol 有独一无二性,故不相等
-
不能与其他值进行运算(Symbol 值不能与其他类型的值进行运算,会报错)
let sym = Symbol('My Symbol'); let str1 = "your symbol is " + sym; //报错TypeError: can't convert symbol to string
-
可显示转换为字符串
let sym = Symbol('My symbol'); sym.toString();// 'Symbol(My symbol)', 这里打印结果并不是Symbol 里面的字符串,切记
-
Symbol 可以转换为bool值,但不能转换为数值
let sym = Symbol(); Boolean(sym) // true Number(sym) //报错 Uncaught TypeError: Cannot convert a Symbol value to a number
九、类(class)的声明、继承(extend)
1、声明类与继承 class、 extend、 ES6 class 与 ES5 function的区别及联系
-
定义:class(类) 作为对象的模板被引入,可以通过class关键字定义类。class的本质是function,它可以看做是一个语法糖,让对象原型的写法更加清晰,更像面向对象的语法。
//1、匿名类 let Example = class { constructor(a) { this.a = a; } } //2、命名类 let Example = class Example { constructor(a) { this.a = a; } }
-
类定义不会被提升,这意为着必须在访问前对类进行定义,否则就会报错。不可重复声明。注意类中的方法不需要用
function
关键字,方法不能加分号。class Example { constructor() { console.log('我是constructor'); } } new Example();// 我是constructor //注意:constructor方法是类的默认方法,创建类的实例化对象时被调用,class 的实例化必须通过 new 关键字
-
通过 extends 关键字实现类的继承
- 1)继承基本语法
class Point { } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); //调用父类的 constructor(x,y),这里子类必须先调用 super,否则会报错 this.color = color; } toString() { return this.color + '' + super.toString(); } } //注意:子类必须在constructor 方法中调用 super方法,否则在新建实例,即new对象时会报错。 //即调用super之后子类才可以使用this关键字
- 2)静态方法的继承
// 父类的静态方法,也会被子类继承 class A { static hello() { console.log("Hello world") } } class B extends A { } B.hello(); //静态的不用new就可以调用
十、ES6 的新特性
1、ES6 有哪些新特性?
- 1、let const 命令
- 2、解构赋值
- 3、字符串方法模板字符串
- 4、默认函数、剩余函数、箭头函数
- 5、Symbol
- 6、Set 和 Map
- 7、模块化
- 8、Promise
- 9、class继承
- 10、数组的扩展
十一、Promise 对象
1、说明:
Promise 是异步编程的一种解决方案,从语法上来说,Promise 是一个对象,从它可以获取异步操作的消息。
2、特点:
-
Promise 异步操作有三种状态:
-
pending (进行中)、fullfilled (已成功)、rejected (已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
-
Promise 对象只有从 pending 变为 fulfilled 和 从 pending变为 rejected的状态改变,只要处于 fullfilled 和 rejected,状态就不会再改变了,即resolved(已定型)
-
-
示例代码:
const p1 = new Promise(function (resolve, reject) { resolve('success1'); resolve('success2'); }); const p2 = new Promise(function (resolve, reject) { resolve('success3'); reject('reject'); }) p1.then(function (value) { console.log(value);//success1 }); p2.then(function (value) { console.log(value);//success3 })
3、缺点:
- 无法取消 Promise, 即一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一阶段(刚刚开始还是即将完成)
十二、异步编程
1、generator(异步编程、yield、next()、await 、async)
-
说明:
- Generator函数,可以通过
yield
关键字,把函数的执行流挂起,为改变执行流提供了可能,从而为异步编程提供了解决方案。 - Generator函数有区别于普通函数的地方:一是在
function
后面,函数名有个*
,函数内部有yield
表达式。其中*
用来函数为 Generator 函数,yield用来定义函数内部的状态。
- Generator函数,可以通过
-
基本用法
function* sendParameter() { console.log("start"); var x = yield '2'; console.log("one:" + x); var y = yield '3'; console.log("two:" + y); console.log("total:" + (x + y)); } var sendp1 = sendParameter(); sendp1.next(); // strat // {value: "2", done: false} sendp1.next(); // one:undefined // {value: "3", done: false} sendp1.next(); // two:undefined // total:NaN // {value: undefined, done: true}
-
基本用法(传入参数)
var sendp2 = sendParameter(); sendp2.next(10); // strat // {value: "2", done: false} sendp2.next(20);// 传入的参数,会作为上一个yield的返回值 // one:20 // {value: "3", done: false} sendp2.next(30); // two:30 // total:50 // {value: undefined, done: true} //一般情况下, next 方法不传入参数的时候,yield表达式的返回值是undefined。当next传入参数的时候,该参数会作为上一步yield的返回值 //yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数
2、Async 和 Await
-
async
是ES7 才有的与异步操作有关的关键字,和Promise
,Generator
有很大的关联。async
函数返回一个Promise
对象,可以使用then
方法添加回调函数。async function helloAsync() { return "helloAsync"; } console.log(helloAsync());// 返回Promise 对象,即: Promise {<resolved>: "helloAsync"} helloAsync().then(v => { console.log(v) });// helloAsync
-
await
操作符用于等待一个Promise
对象,它只能在异步函数 async function 内部使用。返回Promise 对象的处理结果。function testAwait(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, 3000); }) } async function helloAsync() { //等待Promise对象有返回,即等待其resolve var x = await testAwait("hello world"); //等到Promise 对象resolved,则会执行下面代码,即3秒后打印结果 console.log(x); } //3秒后打印 hello world helloAsync();//hello world
-
注意:
- 如果等待的不是Promise 对象,则应该返回本身。
- 如果一个 Promise 被传递给一个 await 操作符,await将等待Promise 正常处理完成并返回其处理结果。
十三、练习题:
1、说出下面程序的运行结果
(function (x, f = () => x) {
var x;
var y = x;
x = 2;
return [x, y, f()];
})(1)
//[2,1,1]
//结果集第三个值为1,原因是箭头函数的作用域等于定义时的作用域。
2、说出下面程序的运行结果
(function () {
console.log(
[
(() => this.x).bind({ x: "inner" })(),
(() => this.x)()
]
)
}).call({ x: "outer" })
//['outer','outer']
//原因是:箭头函数的作用域等于其定义时的作用域,所以通过 bind 设置的this是无效的
3、下面程序的执行结果为何?
let x, { x: y = 1 } = { x }; y;
//结果为1,首先定义 x,然后在赋值的时候,会执行一次 y=1,最后返回 y
4、 下面程序的执行结果为何?
(function () {
let a = this ? class b { } : class c { };
console.log(typeof a, typeof b, typeof c);
})()
// function undefined undefined
//在定义函数变量时,函数名称只能在函数体中生效
5、说出下面程序的执行结果
[...[...'...']].length //3
//扩展运算符 ... 将后面的对象转换为数组,具体做法是: [...<数据>]
//例如 [...'abc'] 等价于 ['a','b','c']
//所以
[...'...'] // [".", ".", "."]
[...[...'...']] //[".", ".", "."]
6、如何实现对象 o3 对 对象 o2 的复制,但是只复制o2 自身的属性,不复制它的原型对象 o1 的属性?
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let o3 = { ...o2 };
7、async 函数有几种声明方式?
//1、通过函数声明
async function foo() { }
//2、通过表达式声明
var bar = async function () { }
//3、通过对象声明
var obj = { async bazfunction() { } }
//4、通过箭头函数声明
var fot = async () => { }
8、在async中,如何处理错误语句?
-
1) 使用 try ...catch 包住可能会出错的部分。
async function demo() { try { await doDemoThing(); } catch (err) { console.log(err); } }
-
2)对可能要出错的异步操作添加catch回调函数
async function demo() { await doSomeThing().catch(err=>console.log(err)) }
9、Iterator 的作用是什么?
- 1)为各种数据提供一个统一的,简便的访问接口。
- 2)使得数据结构的成员能够按某种次序排列。
- 3)ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要提供 for...of 使用。
10、for...in 循环的缺点是什么?
- 1)数组的键名是数字,但是 for...in循环以字符串作为键名
- 2)for...in循环不仅遍历数字键名,还好遍历手动添加的其他键,甚至包括原型链上的键。
- 3)某些情况下,for...in循环会以任意顺序遍历键名。
总之: for...in循环会以任意顺序遍历键名。
11、请说出扩展运算符与剩余操作符之间的区别
简单的说,在某种程度上,剩余操作符和扩展运算符相反,扩展运算符会使数组“展开”成多个元素,剩余操作符会手机多个元素并“压缩”成一个单一的元素。
12、ES6 Module 能否使模块按需加载(lazyload)?
- ES6 module (import/export语句)是静态的,所以无法用于按需加载,import 是静态执行的,不能使用表达式或者变量,另外 import命令有提升的效果,提升到头部首先执行。
- 如果 if 语句中加入 import语句就会报错。
- 可以使用 import() 方法,返回一个 Promise 对象。
13、使用json 方式实现深拷贝
let arr1 = [1, 2, 3, 4];
let arr2 = JSON.parse(JSON.stringify(arr1));