es6 学习心得
Posted zi_chil
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了es6 学习心得相关的知识,希望对你有一定的参考价值。
1、let/ const声明变量(项目中常用)
之前使用var关键字声明变量,无论声明在何处,都会被视为声明在它的作用域的最顶部(不在大括号内{}即在全局作用域的最顶部),而在es6之前javascript只有函数作用域和全局作用域。这就是变量提升。
console.log(a); //输出undefined if (true) { var a = 1; }
上面的代码实际上是:
var a; console.log(a); //输出undefined if (true) { a = 1; }
(1)let声明变量
let实际上为JavaScript新增了块级作用域。let声明的变量只在它所在的代码块内有效。
将上面变量提升的例子稍微变化一下:可以看出let声明的a只在代码块内有效。
if(true) { var a = 1; } console.log(a) //输出1
if(true) { let a = 1; } console.log(a) //报错ReferenceError: a is not defined
再看一个常见的例子:
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10
每次循环的i指向的其实都是同一个i,很显然最后的结果是10。那么如何达到想要的效果?
·闭包(ES5)
var a = []; for (var i = 0; i < 10; i++) { a[i] = (function (num) { return function () { console.log(num); }; })(i) } a[6](); // 6
·let声明变量(ES6)
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
变量i
是let
声明的,当前的i
只在本轮循环有效,所以每一次循环的i
其实都是一个新的变量。
使用let声明变量的话,不存在变量提升的情况,必须在声明以后才使用,否则会报错,这在语法上称为“暂时性死区”。并且let不允许在相同作用域内重复声明变量。
(2)const声明变量
const声明的变量是一个只读变量,一旦声明就必须马上初始化并且不能改变值,因此如果用const只声明而不赋值也会报错。
const的作用域与let相同,只在当前的块级作用域内有效。同样的,也不能变量提升,存在暂时性死区,不能重复声明。
本质:
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const
只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only
总结:因此,可以知道什么时候用let什么时候用const:值会改变的变量用let声明,值是一个不会改变的常量就用const声明。
2、箭头函数(项目中常用)
Es6使用“箭头”(=>)定义函数。箭头函数使得表达更简洁。
箭头函数的写法:
const fn = (a, b) => a + b;
就相当于ES5标准下的:
var fn = function (a, b) { return a + b; }
箭头函数还可以与变量解构结合使用。
const full = ({ first, last }) => first + \' \' + last;
// 等同于 function full(person) { return person.first + \' \' + person.last; }
使用箭头函数有几点需要注意。
(1) 函数体内的this对象,是定义时所在的对象,而不是使用时所在的对象。如果在箭头函数内使用this,那么该this是外层的this,换句话说,就是箭头函数里根本没有自己的this,而是引用外层的this。
(2) 不可以当做构造函数,也就是说,不可以使用new命令,否则抛出错误。
(3) 不可以使用arguments对象,该对象在函数体内不存在。如果一定要用,可以使用rest参数代替。
上面的第一点尤其要注意,this对象的指向是可变的,但在箭头函数中,它是固定的。
function foo() { setTimeout(() => { console.log(\'id:\', this.id); }, 100); } var id = 21; foo.call({ id: 42 }); // id: 42
上面代码中,setTimeout的参数是一个箭头函数,该箭头函数生效是在foo函数生成时,而它真正执行则是在100毫秒后。如果是普通函数,执行时的this执行全局对象window,输出21。而箭头函数的this总是绑定在定义生效时所在的作用域,因此输出42。
3、模板字符串
模板字符串解决了使用+号拼接字符串造成的不便利。模板字符串用(`)反引号标识,可以作普通字符串,也可以定义多行字符串,或者在字符串在嵌入变量。
// 普通字符串 `In JavaScript \'\\n\' is a line-feed.`
// 多行字符串 `In JavaScript this is not legal.`
// 字符串中嵌入变量 let name = "Bob", time = "today"; `Hello ${name}, how are you ${time}?`
使用模板字符串表示多行字符串,所以的空格和缩进都会被保留在输出中。
使用${}包裹一个变量或表达式。
4、变量的解构赋值
(1)数组的解构赋值
[基本用法]
let [a, b, c] = [1, 2, 3];
上面代码表示。可以从数组中提取值,按照对应位置,给变量赋值。
本质上,这种写法属于“模式匹配”,只要等号两边模式相同,左边的变量就会被赋予对应的值。
如果解构不成功,变量的值就等于undefined。
let [foo] = []; foo //undefined let [bar, foo] = [1]; foo //undefined
另一种情况是不完全解构,即等号左边的模式只匹配一部分等号右边的数组,这种情况下,解构依然成功
let [x, y] = [1, 2, 3]; x // 1 y // 2
[默认值]
解构赋值允许制定默认值。
let [foo = true] = []; foo // true
es6内部使用严格相等运算符(===),判断一个位置是否有值。只有数组成员严格等于undefined,默认值才会生效。
let [x, y = \'b\'] = [\'a\']; // x=\'a\', y=\'b\' let [x, y = \'b\'] = [\'a\', undefined]; // x=\'a\', y=\'b\' let [x = \'a\'] = [null]; x // null
null不严格等于undefined,因此默认值不生效
(2)对象的解构赋值
解构不仅可以用于数组,还可以应用于对象。
let { bar, foo } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb"
等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。
let { baz } = { foo: "aaa", bar: "bbb" }; baz // undefined
变量没有对应的同名属性,导致取不到值,最后等于undefined
。
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: "aaa", bar: "bbb" }; baz // "aaa" foo // error: foo is not defined
foo
是匹配的模式,baz
才是变量。真正被赋值的是变量baz
,而不是模式foo
。
(3)字符串的解构赋值
const [a, b, c, d, e] = \'hello\'; a // "h" b // "e" c // "l" d // "l" e // "o"
还可以对字符串的length属性解构赋值
let { length: len } = \'hello\'; len // 5
(4)数值和布尔值的解构赋值
解构赋值是,登等号右边是数值或布尔值,则会先转为对象。
let { toString: s } = 123; s === Number.prototype.toString // true let { toString: s } = true; s === Boolean.prototype.toString // true
(5)函数参数的解构赋值
函数的参数也可使用解构赋值。
function add([x, y]) { return x + y; } add([1, 2]); // 3
(6)用途
变量的解构赋值的用途:
·交换变量的值
let x = 1; let y = 2; [x, y] = [y, x];
·从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能把他们放在数组或对象里返回。有了解构赋值,取出这些值非常方便。
// 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example();
// 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example();
·函数参数的定义
解构赋值可以方便地将一组参数与变量名对应。
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]);
// 参数是一组无次序的值 function f({ x, y, z }) { ... } f({ z: 3, y: 2, x: 1 });
·提取数据
解构赋值对提取JSON对象中的数据尤其有用。
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309]
·函数参数的默认值
指定参数的默认值,就避免了在函数内部再写var foo = config.foo || ‘default foo’。
jQuery.ajax = function (url, { async = true, beforeSend = function () {}, cache = true, complete = function () {}, crossDomain = false, global = true, // ... more config }) { // ... do stuff };
5、函数参数的默认值
(1)默认用法
ES6
之前不能给函数参数指定默认值,只能采用变通的方法。
function log(x, y) { y = y || \'World\'; console.log(x, y); } log(\'Hello\') // Hello World log(\'Hello\', \'China\') // Hello China log(\'Hello\', \'\') // Hello World
上面的代码检查函数的参数y有没有赋值,如果没有,则指定默认值为World。但这样的缺点在于,如果参数y赋值了,但对应的布尔值为false,则该赋值不起作用。如上面代码最后一行,y等于空字符,结果还是被改为默认值。
为避免这个问题,通常需先判断参数y是否被复制,如果没有,再等于默认值。
if (typeof y === \'undefined\') { y = \'World\'; }
Es6
允许函数的参数设置默认值,直接写在参数定义的后面。
function log(x, y = \'World\') { console.log(x, y); } log(\'Hello\') // Hello World log(\'Hello\', \'China\') // Hello China log(\'Hello\', \'\') // Hello
(2)与解构赋值默认值结合使用
函数参数默认值可以与解构赋值的默认值,结合起来使用。
function foo({ x, y = 5 }) { console.log(x, y); } foo({}) // undefined 5 foo({ x: 1 }) // 1 5 foo({ x: 1, y: 2 }) // 1 2 foo() // TypeError: Cannot read property \'x\' of undefined
上面的代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数调用时没提供参数,变量x和变量y就不会生成,因此报错。提供函数的默认值就可以避免这种情况。
function foo({ x, y = 5 } = {}) { console.log(x, y); } foo() // undefined 5
上面的代码指定,没有提供参数,函数的参数默认为一个空对象。
// 写法一 function m1({ x = 0, y = 0 } = {}) { return [x, y]; } // 写法二 function m2({ x, y } = { x: 0, y: 0 }) { return [x, y]; }
上面两张写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
// 函数没有参数的情况 m1() // [0, 0] m2() // [0, 0] // x 有值,y 无值的情况 m1({ x: 3 }) // [3, 0] m2({ x: 3 }) // [3, undefined] // x 和 y 都无值的情况 m1({}) // [0, 0]; m2({}) // [undefined, undefined]
(3)作用域
设置了参数的默认值,函数进行声明初始化时,参数会形成单独的作用域,初始化结束,则该作用域消失。
var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2
上面代码,y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里,默认值变量x指向第一个参数x,而不是全局变量x,因此输出的是2。
let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1
上面代码,函数f调用时,参数y=x形成一个单独作用域,在这个作用域里,变量x本身无定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x不影响默认值变量x。
var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined
上面代码,参数x=x形成单独作用域,实际执行的是let x = x,由暂时性死区的原因,这行代码会报错“x未定义”。
如过参数的默认值是一个函数,该函数的作用域也遵守这个规则。
let foo = \'outer\'; function bar(func = () => foo) { let foo = \'inner\'; console.log(func()); } bar(); // outer
上面代码,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里,没有定义变量foo,因此foo指向外层的全局变量foo,因此输出outer。
var x = 1; function foo(x, y = function () { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1
上面代码,函数foo的参数形成一个单独作用域。改作用域里,首先声明变量x,然后声明变量y,y
的默认值是一个匿名函数。这个匿名函数内部的变量x
,指向同一个作用域的第一个参数x
。函数foo
内部又声明了一个内部变量x
,该变量与第一个参数x
由于不是同一个作用域,所以不是同一个变量,因此执行y
后,内部变量x
和外部全局变量x
的值都没变。
var x = 1; function foo(x, y = function () { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1
如果将var x = 3的var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,最后输出2,而外层的全局变量x不受影响。
6、rest参数
引入rest参数(…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象。Rest参数搭配的变量是一个数组,将多余的参数放入数组中。
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。
// arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); }
// rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
上面是rest参数代替arguments变量的例子。比较后可发现,rest参数的写法更自然更简洁。
arguments对象不是数组,是类似数组的对象,为使用数组方法,需先将其转为数组。Rest参数就不存在这个问题,它自身就是数组。
// arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); }
// rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
上面是利用rest参数改写数组push方法的例子。
需注意。Rest参数之后不能再有其他参数,即只能是最后一个参数,否则报错。
函数的length属性返回没有默认值的参数的个数,其中不包括rest参数。
(function (a) { }).length // 1 (function (...a) { }).length // 0 (function (a, ...b) { }).length // 1
7、对象的扩展
(1)Es6中可以直接写入变量和函数,作为对象的属性和方法,使书写更简洁。
const foo = \'bar\'; const baz = { foo }; baz // {foo: "bar"}
// 等同于 const baz = { foo: foo };
此时,属性名为变量名,属性的值为变量的值。
function f(x, y) { return { x, y }; }
// 等同于 function f(x, y) { return { x: x, y: y }; } f(1, 2) // Object {x: 1, y: 2}
方法也可以简写。
在一个模块对外提供接口时非常适合使用简洁写法。
const getName = () => person.name;
const getAge = () => person.age;
commons的写法:
module.exports = {
getName: getName,
getAge: getAge,
};
Es6 modules的写法:
export default { getName, getAge };
(2)若自适应字面量方法定义对象(使用大括号),es5中只能像下面这样定义属性:
var obj = { foo: true, abc: 123 };
在es6中,就可以把表达式放在方括号内定义对象:
let propKey = \'foo\'; let obj = { [propKey]: true, [\'a\' + \'bc\']: 123 };
8、class
生成实例对象的传统方法使通过构造函数,与传统的面向对象语言差异很大,es6引入class类这个概念,通过class关键字可以定义类。Es6的class可以看作是一个语法糖,它的绝大部分功能,es5都可以做到,class写法只是让对象的写法更清晰。
es5生成实例对象:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function () { return this.name; } var person = new Person(\'abc\', 21);
es6用class改写
class Person { constructor(name, age) { this.name = name; this.age = age; } getName() { return this.name } } var person = new Person(\'abc\', 21);
constructor是构造方法,this代表实例对象,es5的构造函数Person,对应的就是Person类的构造方法,定义类方法使不需要function关键字,直接把函数定义放进去,另外方法之间也不需要逗号分隔。
typeof Person // "function" Person === Person.prototype.constructor // true
上面表明,类的数据类型就是函数,类本身事项构造函数。
class Person { constructor() { // ... } getName() { // ... } }
// 等同于 Person.prototype = { constructor() {}, getName() {} };
类的所有方法都定义在类的prototype属性上。因此,类的新方法可以添加在prototype对象上。而Object.assign方法可以仿版地一次想类添加多个方法。
Object.assign(Person.prototype, {
getNmae() {},
getAge() {}
});
9、class的继承
1)Es6中,class可以通过extends关键字实现继承,比起es5的通过修改原型链实现继承方便的多。
class Person { constructor(name, age) { this.name = name; this.age = age; } getName() { return this.name } }
// Student类继承Person类 class Student extends Person { constructor(name, age, gender, classes) { super(name, age); this.gender = gender; this.classes = classes; } getGender() { return this.gender; } }
不用像es5那样需要考虑构造函数继承哈斯原型继承,只需要使用extends关键字并关注一个叫super的方法。
如果不使用super方法,在新建实例时就会报错。
Es5的继承,实质是县创造子类实例对象的this,在将父类的方法添加到this(Parent.apply(this))。 而es6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类构造函数修改this。
如果子类没有定义constructor(),也会被默认添加。即任何一个子类都有constructor方法。
class Student extends Person {} // 等同于 class Student extends Person { constructor(...args) { super(...args); } }
需要注意,只有调用super以后才能使用this,否则报错。因为上面说过,子类实例的构建,是基于父类实例加工,super方法返回父类实例。
1) super关键字既可以当作函数使用也可以当作对象使用。
·Super作为函数调用时,代表父类的构造函数,子类的构造数必须执行一次super函数。
class A {}
class B extends A {
constructor() {
super();
}
}
Super虽然代表了父类A的工业早函数,但返回的是子类B的实例,即super内部的this指的是B,因此super在这里相当于A.prototype.constructor.call(this)。
class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A() // A new B() // B
new.target指向当前正在执行的函数。从上面的代码可以看出:在super执行时,它指向的是子类B的构造函数,即super内部的this指向B
并且,作为函数是,super只能用在子类的构造函数之中,否则报错。
·第二种情况,super作为对象。在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A { p() { return 2; } } class B extends A { constructor() { super(); console.log(super.p()); // 2 } } let b = new B();
上面代码中,子类B的super.p就是把super当做对象使用,此时super在普通方法中,指向A.prototype,因此super.p()就相当于A.prototype.p()。
注意:定义在父类实例上的方法、属性无法通过super调用。
class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } } let b = new B(); b.m() // 2
super.print()虽然调用的是A.prototype.print(),但A.prototype.print()内部的this指向子类B,因此输出2,而不是1。实际上执行的是super.print.call(this)。
如果super作为对象,用在静态方法中,super就指向父类,而不是父类的原型对象。
class Parent { static myMethod(msg) { console.log(\'static\', msg); } myMethod(msg) { console.log(\'instance\', msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); } myMethod(msg) { super.myMethod(msg); } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2
上面的代码,super在静态方法中指向父类,在普通方法中指向父类的原型对象。
10、promise(项目中ajax.js中使用)
promise的作用与回调方法(callback)一样,都是在某种情况下执行设定好的方法。但promise的多重链式调用能使代码更整洁,避免出现“回调地狱”(回调嵌套太多层)。在es6中,promise成为了原生对象可以直接使用。
Ajax请求的传统写法:
改为promise写法:
很显然,promise的写法把异步调用中使用回调函数的场景改为了.then()、.catch()等函数链式调用的方式,基于promise可以把复杂的异步调用方式进行模块化。
Promise的原理分析:
Promise对象共有三个状态,分别是:
·pending(进行中)
·resolved(已完成,又称为fullfilled)
·rejected(已失败)
由异步操作的结果决定当前是什么状态。状态的改变只有两种可能:
·从pending变为fullfilled
·从pending变为rejected
只要这两种情况发生,状态就不会再改变了,因此状态是不能逆向改变的。
构建promise:
Promise的构造函数接受一个函数作为参数,该函数的两个参数分别为resolve和reject两个函数。
·Resolve函数将promise对象的状态由pending变为resolved,异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
·reject函数将状态由pending变为rejected,异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise的实例方法:
Promise对象拥有两个实例方法then()和catch()。
·then()方法接受两个函数作为参数,第一个是状态变为resolved时调用,第二个则是状态变为rejected是调用,第二个函数是可选的。
Promise实例生成以后,可以用then
方法指定resolved
状态和rejected
状态的回调函数,即成功和失败的回调函数。
promise
构造函数中通常都是异步的,所以then
方法往往都先于resolve
和reject
方法执行。这两个函数作为成功和失败的回调函数,都接受promise对象传出的值作为参数。
then()方法将返回一个新的promise。
因此then可以链式调用,在新的对象上添加失败或成功的回调。
·catch()方法的作用是捕获promise的错误。
与then()方法的rejected回调作业几乎一致。
我们知道,如果 Promise 状态已经变成resolved
,再抛出错误是无效的。
上面代码中,Promise 在resolve
语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
promise对象的错误会一直向后传递,直到被捕获,即错误总会被下一个catch
所捕获。then
方法指定的回调函数,若抛出错误,也会被下一个catch
捕获。catch
中也能抛错,则需要后面的catch
来捕获。
因此一般来说,不要在then
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。
这样就能够在下一个catch()中统一处理这些错误。同时catch()也能够捕获then()中抛出的错误,所以建议不使用then()的rejected回调,而是统一使用catch()来处理错误。
跟传统的try/catch
代码块不同的是,如果没有使用catch
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。Promise 内部的错误不会影响到 Promise 外部的代码。
11、Module(项目中常用)
ES6实现了模块功能,可以取代CommonJS和AMD规范,称为浏览器和服务器通用的模块解决方案。
模块功能主要由两个命令构成:export和import。
Export用于规定模块的对外接口,import用于输入其他模块提供的功能。
一个模块即一个独立文件。该文件内部的所有变量,在外部是无法获取的。如果需要读取模块内的某个变量,就必须使用export输出该变量。
(1)export
export可以输出变量:
// profile.js export var firstName = \'Michael\'; export var lastName = \'Jackson\'; export var year = 1958;
或者用大括号指定要输出的一组变量:
// profile.js var firstName = \'Michael\'; var lastName = \'Jackson\'; var year = 1958; export { firstName, lastName, year };
除了输出变量,还可以输出函数或class类
export function multiply(x, y) { return x * y; };
(2)import
使用export定义了,模块对外的几口以后,其他js文件就可以通过import加载这个模块。
// main.js import { firstName, lastName, year } from \'./profile.js\'; function setName(element) { element.textContent = firstName + \' \' + lastName; }
Import接收一堆大括号,里面指定要从其他模块导入的变量名,变量名必须与被导入模块(profile.js)对外接口的名称相同。
Import输入的变量都是只读的,不允许在加载模块的脚本里改写接口。
From指定模块文件的位置,.js后缀可以省略。
Import具有提升效果,会提升到整个模块的头部首先执行。
(3)export default
使用export default为模块指定默认输出。
// export-default.js export default function () { console.log(\'foo\'); }
默认输出一个匿名函数
// import-default.js import customName from \'./export-default\'; customName(); // \'foo\'
加载该模块时,import为函数指定名字。
需要注意的是export default对应的import不需要使用大括号{}。而export对应的import需要使用大括号。
以上是关于es6 学习心得的主要内容,如果未能解决你的问题,请参考以下文章