主要从以下几个方面区分:
1.作用域不同
块级作用域:声明的变量只在该代码块作用域内有效
var没有块级作用域,let、const有块级作用域
for (var i = 0; i < 5; i++) {
console.log(i)
}
console.log('外层')
console.log(i)
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log('外层')
console.log(i)
有些人会有疑问,为什么日常开发中没有显式的声明块级作用域,let/const声明的变量却没有变为全局变量
这个其实也是let/const的特点,ES6规定它们不属于顶层全局变量的属性,这里用chrome调试一下。
可以看到使用let声明的变量x是在一个叫script作用域下的,而var声明的变量因为变量提升所以提升到了全局变量window对象中,这使我们能放心的使用新语法,不用担心污染全局的window对象
2.暂时性死区:只要块级作用域有let、const命令,他们所声明的变量就绑定这个区域,不受外部影响
var a = 10
if (true) {
console.log(a)
var a = 20
}
// 10
以上代码改为以下let或const均报错:
var a = 10
if (true) {
console.log(a)
let a = 20
}
或
var a = 10
if (true) {
console.log(a)
const a = 20
}
// 报错:Uncaught ReferenceError: Cannot access 'a' before initialization
3.是否存在变量提升
var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined。
let和const不存在变量提升问题(注意这个‘问题’后缀,其实是有提升的,只不过是let和const具有一个暂时性死区的概念,即没有到其赋值时,之前就不能用
),即它们所声明的变量一定要在声明后使用,否则报错。
console.log(a)
var a = 10
// undefined
console.log(a)
let a = 10 || const a = 10
// 报错:Uncaught ReferenceError: Cannot access 'a' before initialization
4.能否重复声明
var声明的可以重复声明, let和const在同一作用域不允许重复声明变量。其中const声明一个只读的常量(因为如此,其声明时就一定要赋值,不然报错)。一旦声明,常量的值就不能改变。
var a = 10
var a = 20
console.log(a) // 20
var a = 10
let a = 20 || const a = 20
console.log(a) // 报错:Uncaught SyntaxError: Identifier 'a' has already been declared
5.变量能否被修改
①var、let声明的变量可以被修改,const声明的常量不可修改。当然如果声明的是一个引用类型(如对象),则不能改变它的内存地址,但是可以是对象内属性可变。 如何使const声明的对象内属性不可变,只可读呢?如果const声明了一个对象,对象里的属性是可以改变的。
var a = 10 || let a = 10
a = 20
console.log(a) // 20
const a = 10
a = 20
console.log(a) // 报错:Uncaught TypeError: Assignment to constant variable.
//因为const声明的obj只是保存着其对象的引用地址,只要地址不变,就不会出错。
const obj={name:'蟹黄'};
obj.name='同学';
console.log(obj.name);//同学
使用Object.freeze(obj) 冻结obj,就能使其内的属性不可变,但它有局限,就是obj对象中要是有属性是对象,该对象内属性还能改变,要全不可变,就需要使用递归等方式一层一层全部冻结。
②const声明变量的时候必须赋值,否则会报错,同样使用const声明的变量被修改了也会报错
【建议】 在日常开发中,我的建议是全面拥抱let/const,一般的变量声明使用let关键字,而当声明一些配置项(类似接口地址,npm依赖包,分页器默认页数等一些一旦声明后就不会改变的变量)的时候可以使用const,来显式的告诉项目其他开发者,这个变量是不能改变的(const声明的常量建议使用全大写字母标识,单词间用下划线),同时也建议了解var关键字的缺陷(变量提升,污染全局变量等),这样才能更好的使用新语法。
箭头函数的特点
(1) ES6 允许使用箭头 => 定义函数
var f = v => v
// 等同于 ES5 的
var f = function (v) {
return v
}
(2) 如果箭头函数不需要参数或需要多个参数,就使用圆括号代表参数部分。
var f = () => 5
// 等同于 ES5 的
var f = function () {
return 5
}
var sum = (numl, num2) => numl + num2
// 等同于 ES5 的
var sum = function (numl, num2) {
return numl + num2
}
(3) 箭头函数使得表达更加简洁
const isEven = n => n % 2 === 0
const square = n => n * n
var result = values.sort((a, b) => a - b)
// 等同于 ES5 的
var result = values.sort(function (a, b) {
return a - b
})
箭头函数的应用
应用一:替代ES5需要显示声明保存this的变量
箭头函数替代了以前需要显式的声明一个变量保存this的操作,使得代码更加的简洁。
在数组的迭代中使用箭头函数更加简洁,并且省略了return关键字
应用三:箭头函数可以与解构结合使用
const full = ({ first , last }) => first + ' ' + last;
// 等同于 ES5 的
function full(person) {
return person.first + ' ' + person.last;
}
应用四: React中的JSX语法:事件监听时传入箭头函数(推荐)
因为 onClick 中要求我们传入一个函数,那么我们可以直接定义一个箭头函数传入:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "你好啊,李银河"
}
}
render() {
return (
<div>
<button onClick={() => this.btnClick()}>点我一下(React)</button>
<button onClick={() => this.btnClick()}>也点我一下(React)</button>
</div>
)
}
btnClick() {
console.log(this);
console.log(this.state.message);
}
}
箭头函数与普通函数的主要区别
箭头函数和普通函数的样式不同,箭头函数语法更加简洁、清晰,箭头函数是=>定义函数,普通函数是function定义函数。
箭头函数会捕获其所在上下文的 this 值,作为自己的 this 值,定义的时候就确定并固定了。
箭头函数没有原型prototype,箭头函数不能作为构造函数使用,也不能使用new关键字(因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会改变,作为构造函数其的this要是指向创建的新对象)。
箭头函数没有自己的arguments。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。(建议使用更好的语法,剩余运算符替代)
call、apply、bind 并不会影响其 this 的指向。
箭头函数不能当作 Generator 函数,不能使用 yield 关键字。
[注意] 但是在某些情况下需要避免使用箭头函数,不要在可能改变this指向的函数中使用箭头函数
场景1
值得注意的是makeRequest后面的function不能使用箭头函数,因为这样它就会再使用上层的this,而再上层是全局的执行上下文,它的this的值会指向window,所以找不到变量a返回undefined。
场景2
类似Vue中的methods,computed中的方法,生命周期函数,Vue将这些函数的this绑定了当前组件的vm实例,如果使用箭头函数会强行改变this,因为箭头函数优先级最高(无法再使用call,apply,bind改变指向)
解构 :是将一个数据结构分解为更小的部分的过程。ES6 中,从数组和对象中提取值,对变量进行赋值。
解构在日常开发中的使用场景
(1)简化变量声明操作
// ES5
var foo = 1
var bar = 2
var baz = 3
// ES6
let [foo, bar, baz] = [1, 2, 3]
(2)变量交换:
看起来如同镜像。赋值语句的左侧的解构模式,右侧是临时创建的数组字面量。x 被赋值为数组中的 y,y 被赋值为数组中的 x。
let x = 1;
let y = 2;
[x, y] = [y, x]
// x = 2, y = 1
(3)字符串解构
const [a, b, c, d, e] = 'hello'
// a => h
// b => e
// c => l
// d => l
// e => o
(4)对象解构
①常规解构
var obj = { x: 1, y: 2, c: 1 }
let { x, y } = obj
// x = 1
// y = 2
【注意】ES6的解构赋值虽然好用。但是要注意解构的对象不能为undefined、null,否则会报错。故在项目开发中最好给被解构的对象一个默认值。
const {a,b,c,d,e} = obj || {};
②动态对象键
假设您要解包其键是动态的对象。解构是不可能的,因为您不能确定键名的有效性吗?
不!对象键可以在解构时动态分配,提供额外的灵活性。
(5)函数参数解构
基础示例1
const xueyue = {
name: '雪月',
age: 18,
}
function getAge({ name, age }) {
return `${name}今年${age}岁`
}
getAge(xueyue) // 雪月今年18岁
①避免过多的函数参数
//错误示例
function myFunction(employeeName,jobTitle,yrExp,majorExp){
return `${employeeName} is working as ${jobTitle} with ${yrExp} years of experience in ${majorExp}`
}
//output be like John is working as Project Manager with 12 year of experience in Project Management
// you can call it via
console.log(myFunction("John","Project Manager",12,"Project Management"))
// ***** PROBLEMS ARE *****
// Violation of 'clean code' principle
// Parameter sequencing is important
// Unused Params warning if not used
// Testing need to consider a lot of edge cases.
//正确示例
function myFunction({employeeName,jobTitle,yrExp,majorExp}){
return `${employeeName} is working as ${jobTitle} with ${yrExp} years of experience in ${majorExp}`
}
//output be like John is working as Project Manager with 12 year of experience in Project Management
// you can call it via
const mockTechPeople = {
employeeName:"John",
jobTitle:"Project Manager",
yrExp:12,
majorExp:"Project Management"
}
console.log(myFunction(mockTechPeople))
// ES2015/ES6 destructuring syntax is in action
// map your desired value to variable you need.
②解构时重命名简化命名
有的后端返回的键名特别长,你可以这样干
// bad
setForm (data) {
this.one = data.aaa_bbb_ccc_ddd
this.two = data.eee_fff_ggg
}
// good
setForm ({aaa_bbb_ccc_ddd, eee_fff_ggg}) {
this.one = aaa_bbb_ccc_ddd
this.two = eee_fff_ggg
}
// best
setForm ({aaa_bbb_ccc_ddd: one, eee_fff_ggg: two}) {
this.one = one
this.two = two
}
③解构时设置默认值
// bad
setForm ({name, age}) {
if (!age) age = 16
this.name = name
this.age = age
}
// good
setForm ({name, age = 16}) {
this.name = name
this.age = age
}
(6)忽略值
数组通常携带大量数据。很多时候,只需要部分数据进行进一步处理。因此,在解构数组时,您可以有选择地解压缩值,忽略不需要的值。如果您希望值保持不变,只需写一个逗号。
(7)分配剩余值
大多数开发人员都会知道 rest 参数。一个函数的参数以 3 个点为前缀,接受无数个参数并将它们解析为一个数组。
但是你知道休息模式也可以用于解构吗?通过在变量前加上 3 个点,您可以将所有剩余的值解包到其中。
这适用于对象和数组解构。尽管知道对于对象的提案目前处于第 4 阶段,这意味着它将正式包含在 ECMAScript 的下一次迭代中。
(8)组合数组和对象解构
对象和数组解构本身就很强大,但将两者结合起来可以为您提供开发超能力。如果您面对一个里面有对象的数组,您可以使用这种技术直接解包嵌套的对象。
(9)重命名变量
每个开发人员都见过至少可以说是模棱两可的对象键。键经常包含拼写错误或与它们的值没有明确的关系。为了克服这个问题,您可以在解构对象时设置一个新的变量名称。解压缩值时,只需写一个冒号,后跟新名称。
(10)默认值
如果在编写代码时有一个保证,那就是您不能信任数据。值可能会意外更改,并且无法避免边缘情况。
因此,提供默认值或回退值通常是一个好主意。解构时设置默认值非常简单。只需在变量名后写一个 = 符号,并提供默认值。当数组或对象中的值未定义时——任何另一个空值都将被解包——默认值被分配给变量。
(11)解构正则表达式
正则表达式用于定位字符串中的模式。当在 javascript 中执行正则表达式 (RegExp.exec() ) 时,匹配项将作为字符串数组返回。
使用数组解构,您可以直接将 RegExp 匹配解包到所需的变量。在下面的例子中,我们将一个邮件地址分解成不同的部分,并直接将它们分配给变量。
(12)嵌套解构
对象和数组并不总是平坦的。您知道您不需要单独的解构赋值来解压值吗?赋值可以嵌套在一个子句中以直接达到所需的级别。
1.10 个 JS 解构赋值相关的知识点
2.JavaScript 解构赋值实用指南
ES6 引入了 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入其中。
比较下面的两种写法可以发现, rest 参数的写法更自然也更简洁。
例1:
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort()
}
// 使用 rest
const sortNumbers = (...numbers) => numbers.sort()
例2:
大家可能在实际开发中遇到过这种问题,一个函数,传入参数的个数是不确定的,这就可以用ES6的剩余参数
function fn (name, ...params) {
console.log(name)
console.log(params)
}
fn ('林三心', 1, 2) // 林三心 [ 1, 2 ]
fn ('林三心', 1, 2, 3, 4, 5) // 林三心 [ 1, 2, 3, 4, 5 ]
扩展运算符( spread ) 是三个点(…) 如同 rest 参数的逆运算 将一个数组转为用逗号分隔的参数序列
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
(1)取代 apply 方法
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f(...args);
下面是扩展运算符取代 apply 方法的一个实际例子 应用 Math.max 方法简化求出数组中的最大元素
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77)
(2)合并数组
扩展运算符提供了数组合并的新写法。
// ESS
[1, 2].concat(more)
// ES6
[1, 2, ...more]
(3)对象的拷贝
对象的扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let z = { a: 3, b: 'bb' }
let n = { ...z }
n // { a: 3, b: 'bb' }
n === z // false
【特别注意】: …扩展对象,只能做到当对象属性是 基本数据类型 才是 深拷贝,如果是 引用数据类型,那就是浅拷贝。
let z = { a: 3, b: 'bb', c: { name: 'ccc' } }
let n = { ...z }
n // { a: 3, b: 'bb', c: { name: 'ccc' } }
n === z // false
n.c === z.c // true
// n.c 跟 z.c 是同一个引用地址
const name = '雪月'
// ES5写法
const obj = {
name: name,
f: function () {
console.log(this.name)
},
}
// ES6简写
const obj2 = {
name,
f() {
console.log(this.name)
},
}
obj.f() // 雪月
obj2.f() // 雪月
使用 vue 的同学是不是感到很熟悉
new Vue({
el: '#app',
data() {
return {
list: [],
}
},
})
const name = obj && obj.name;
吐槽: ES6中的可选链操作符会使用么?
改进
const name = obj?.name;
当给对象添加属性时,如果属性名是动态变化的,该怎么处理。
let obj = {};
let index = 1;
let key = `topic${index}`;
obj[key] = '话题内容';
吐槽: 为何要额外创建一个变量。不知道ES6中的对象属性名是可以用表达式吗?
改进
let obj = {};
let index = 1;
obj[`topic${index}`] = '话题内容';
(1).关于拼接字符串的吐槽
const name = '小明';
const score = 59;
let result = '';
if(score > 60){
result = `${name}的考试成绩及格`;
}else{
result = `${name}的考试成绩不及格`;
}
吐槽
像你们这样用ES6字符串模板,还不如不用,你们根本不清楚在${}
中可以做什么操作。在${}
中可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。
改进
const name = '小明';
const score = 59;
const result = `${name}${score > 60?'的考试成绩及格':'的考试成绩不及格'}`;
参考阅读:
1.【面试】1069- 前端必知必会的 10 道 Promise 面试题
一.简单介绍下Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。(当然了也可以简单介绍promise状态,有什么方法,callback存在什么问题等等,这个问题是比较开放的)
二.使用Promise进行顺序(sequence)处理。
function sequenceTasks(tasks) {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(() => task).then(pushValue);
}, Promise.resolve());
}
三.Promise链上返回的最后一个Promise出错了怎么办?
catch在promise链式调用的末尾调用,用于捕获链条中的错误信息,但是catch方法内部也可能出现错误,所以有些promise实现中增加了一个方法done,done相当于提供了一个不会出错的catch方法,并且不再返回一个promise,一般用来结束一个promise链。
done() {
this.catch(reason => {
console.log('done', reason);
throw reason;
});
}
四.Promise.then在Event Loop中的执行顺序。(可以直接问,也可以出具体题目让面试者回答打印顺序)
JS中分为两种任务类型:macrotask和microtask,其中macrotask包含:主代码块,setTimeout,setInterval,setImmediate等(setImmediate规定:在下一次Event Loop(宏任务)时触发);microtask包含:Promise,process.nextTick等(在node环境下,process.nextTick的优先级高于Promise)Event Loop中执行一个macrotask任务(栈中没有就从事件队列中获取)执行过程中如果遇到microtask任务,就将它添加到微任务的任务队列中,macrotask任务执行完毕后,立即执行当前微任务队列中的所有microtask任务(依次执行),然后开始下一个macrotask任务(从事件队列中获取) 浏览器运行机制可参考这篇文章
五.实现一个简单的,支持异步链式调用的Promise类。
这个答案不是固定的,可以参考最简实现 Promise,支持异步链式调用
六.如何停止一个Promise链?
在要停止的promise链位置添加一个方法,返回一个永远不执行resolve或者reject的Promise,那么这个promise永远处于pending状态,所以永远也不会向下执行then或catch了。这样我们就停止了一个promise链。
Promise.cancel = Promise.stop = function() {
return new Promise(function(){})
}
七.阐述Promise的一些静态方法。
Promise.deferred、Promise.all、Promise.race、Promise.resolve、Promise.reject等
八.Promise存在哪些使用技巧或者最佳实践?
九.Promise存在哪些缺点。
async/await优点:
a. 用同步的形式书写异步代码,代码阅读相对容易。
b. 对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面。
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(222)
}, 2222)
})
};
async function f() {
try {
if ( await a() === 222) {
console.log('yes, it is!') // 会打印
}
} catch (err) {
// ...
}
}
c. 处理复杂流程时,在代码清晰度方面有优势
async/await缺点:
a. 无法直接处理promise返回的reject对象,要借助try…catch…
该缺点可以查看
1.如何优雅处理 async await 错误——解读小而美的 await-to-js 库
b. 用 await 可能会导致性能问题,因为 await 会阻塞代码, 也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
//promise
Promise.all([ajax1(), ajax2()])
c. try…catch…内部的变量无法传递给下一个try…catch…,Promise和then/catch内部定义的变量, 能通过then链条的参数传递到下一个then/catch,但是async/await的try内部的变量,如果用let和const定义则无法传递到下一个try…catch…,只能在外层作用域先定义好。
但async/await确确实实是解决了promise一些问题的。更加灵活的处理异步
promise的一些问题:
a. 一旦执行,无法中途取消,链式调用多个then中间不能随便跳出来。
b. 错误无法在外部被捕捉到,只能在内部进行预判处理.如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
c. 吞掉错误或异常,错误只能顺序处理,即便在Promise链最后添加catch方法,依然可能存在无法捕捉的错误(catch内部可能会出现错误)
Promise内部如何执行,监测起来很难,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
d.阅读代码不是一眼可以看懂,你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。
1.如何优雅处理 async await 错误——解读小而美的 await-to-js 库
Map 和 Set 两种数据结构在ES6的作用
推荐阅读: