参数默认值
ECMAScript2015当中为函数的形参列表扩展了一些非常有用的新语法,我们这里分别来看一下。
首先是参数的默认值,以前我们想要为函数中的参数去定义默认值我们需要在函数体中通过逻辑代码来实现,例如下面的foo函数有一个enable参数。如果我们需要他的默认值是true,这里我们就需要逻辑判断来去决定是否使用默认值。
function foo (enable) {
enable = enable || true;
console.log(enable); // true
}
foo(true);
这里也有一个很多人都会犯错的地方,很多人喜欢使用短路运算的方式去设置默认值, 仔细一点你就会发现,这里这种情况其实不能使用短路运算的方式来去设置默认值,因为这会导致如果我们传入false时,也会导致使用默认值,这是很明显的错误。
function foo (enable) {
enable = enable || true;
console.log(enable); // true
}
foo(false);
正确的做法就是判断我们这个enable是否是undefined,然后去决定是否使用默认值,因为参数默认值的定义呢就是在我们没有去传递实际参数时所使用的的一个值。
没有传递实参,我们得到的就是一个undefined,所以这里应该判断是否为undefined
function foo (enable) {
enable = enable === undefined ? true : enable;
console.log(enable); // false
}
foo(false);
有了参数默认值这个新功能以后,这一切就会变得简单的多,我们可以直接在形参的后面直接通过等号去设置一个默认值就ok了。这里设置的默认值只会在我们调用时没有传递实参或者实参传递的是一个undefined时才会被使用。
function foo (enable = true) {
console.log(enable); // false
}
foo(false);
需要注意的是如果说有多个参数的话,那带有默认值的这种形参一定要出现在参数列表的最后,因为我们的参数是按照次序传递的。如果带有参数默认值的这种参数不在最后的话,将无法正常工作,因为程序没办法确定是哪一个参数没有传递。
function foo (bar, enable = true) {
console.log(enable); // false
}
foo(false);
剩余参数
在ECMAScript中很多方法都可以传递任意个数的参数,例如console.log
方法他可以接收任意个数的参数,并且最终会把这些参数打印在同一行当中。
对于未知个数的参数,以前我们都是使用arguments对象去获取,arguments对象实际上是一个伪数组,在ES2015当中新增了一个...
操作符,那这种操作符有两个作用。
这里我们用到的是他的reset作用,也就是剩余操作符,我们可以在函数的形参前面加上...
, 那此时这个形参args就会以数组的形式去接收从当前这个参数的位置开始往后所有的实参。
这种方式就可以取代以前通过arguments对象去接收无限参数这种一种操作,那此时如果我们再去调用我们这里的函数,我们传递的所有的参数都将被放在args数组当中。
// function foo() {
// console.log(arguments); // 参数集合
// }
function foo (...args) => {
console.log(args); // 参数集合
}
foo(1, 2, 3, 4);
因为接收的是所有的参数,所以这种操作符只能出现在我们形参列表的最后一位,而且只可以使用一次。
展开数组
...
操作符除了可以收起剩余数据这种reset用法,他还有一种spread的用法,意思就是展开。
那这个展开操作符的用法他的用途有很多,这里我们先来了解与函数相关的数组参数展开。
例如我们这里有一个数组,我们想要把数组当中的每一个成员按照次序传递给consult.log方法,最原始的办法是通过下标一个一个去找到数组当中的每一个元素,分别传入到console.log方法当中。
如果说数组当中元素个数是不固定的。那一个个传递的方式就行不通了,我们就必须要换一种方式。
const arr = ['foo', 'bar', 'baz'];
console.log(arr[0], arr[1], arr[2]);
以前面对这种问题一般我们都是使用函数对象的apply方法去调用函数,因为这个方法可以以数组的方式去接收实参列表,我们这里就是console.log.apply,这个方法的第一个参数是this的指向,这里log是console调用的,所以第一个参数传入console, 第二个参数就是是参列表的数组。
console.log.apply(console, arr);
在ES2015当中就没有必要这么麻烦了,我们可以直接去调用console的log方法,然后通过...
的操作符展开这里的数组。...
操作符会把数组当中的每一个成员按照次序传递到列表当中。
console.log( ...arr );
这样就大大简化了我们需要的操作。
箭头函数
在ECMAScript当中简化了函数表达式的定义方式允许我们使用=>这种类似箭头的符号来去定义函数,那这种函数一来简化了函数的定义,二来多了一些特性我们具体来看。
传统我们来定义一个函数需要使用function关键词,现在我们可以使用ES2015来去定义一个完全相同的函数。
function inc (number) {
return number + 1;
}
const inc = n => n + 1;
此时你会发现,相比于普通的函数,剪头函数确实大大简化了我们所定义函数这样一些相关的代码。
我们这里来看一下剪头函数的语法,剪头函数的左边是参数列表,如果有多个参数的话可以使用()包裹起来,在剪头的右边是函数体。只有一句表达式,执行结构会作为返回值返回。
如果在这个函数的函数体内需要执行多条语句,同样可以使用{}去包裹。不过一旦使用了{}返回值就需要手动通过return关键词去返回。
const inc = (n , m) => {
return n + 1;
};
使用剪头函数最主要的变化就是极大的简化了我们回调函数的编写。例如我们这里定义一个数组,如果说我们想要筛选出数组中所有的基数,就可以使用数组对象的filter方法,然后传入一个包含筛选逻辑的函数。
const arr = [1, 2, 3, 4, 5, 6, 7];
arr.filter(function(item) {
return item % 2;
})
arr.filter(i => i % 2);
对比一下普通函数和剪头函数的写法你会发现,使用剪头函数会让我们的代码更简短,而且更易读。
对象字面量的增强
相比于普通函数,箭头函数还有一个很重要的变化就是不会改变this的指向。
这里我们定义一个person对象,然后在这个对象当中去定义一个name属性,然后我们再去定义一个sayHi的方法,这个方法中我们就可以使用this去获取当前对象。
因为在普通函数中this始终会指向调用这个函数的对象。我们把this里面的name打印出来。
const person = {
name: 'yd',
sayHi: function() {
console.log(this.name);
}
}
person.sayHi(); // yd
我们这里把sayHi改为箭头函数的方式。这个时候打印出来的name就是undefined
const person = {
name: 'yd',
sayHi: () => {
console.log(this.name);
}
}
person.sayHi(); // undefined
这就是箭头函数和普通函数最重要的区别,在剪头函数当中没有this的机制。所以说不会改变this的指向。也就是说在剪头函数的外面this是什么,在里面拿到的就是什么,任何情况下都不会发生改变。
在这里我们再添加一个sayHiAsync的方法,这是一个普通的函数,在这个方法当中需要延迟一秒再去打印消息,可以使用setTimeout来实现。
此时如果setTimeout传递进去的是一个普通的函数表达式,在这个函数内部就没有办法拿到当前作用域的this,因为这个函数在setTimout里面会放在全局对象上执行,所以他里面是拿不到当前作用域里面的this对象,拿到的应该是全局对象。
const person = {
name: 'yd',
sayHi: () => {
console.log(this.name);
},
sayHiAsync: function() {
setTimeout(function() {
console.log(this.name);
}, 1000)
}
}
person.sayHiAsync(); // undefined
很多时候为了解决这个问题我们会定义一个变量self来存储当前this, 借助闭包这样一个机制去在内部使用self。
const person = {
name: 'yd',
sayHi: () => {
console.log(this.name);
},
sayHiAsync: function() {
const self = this;
setTimeout(function() {
console.log(self.name);
}, 1000)
}
}
person.sayHiAsync(); // yd
如果我们这里使用的是箭头函数就不用这么麻烦了,因为在剪头函数当中this始终指向的都是当前作用域里面的this,以后但凡你的代码中需要使用变量存储this的情况,都可以使用剪头函数来去避免。
对象字面量的增强
对象是我们在ECMAScript当中最常用的数据结构,ECMAScript当中升级了我们对象字面量的语法。
传统的字面量要求我们必须在{}里面使用属性名:属性值这种语法。即便说我们属性的值是一个变量,那也必须是属性名:变量名
const bar = '123';
const obj = {
key: 'value',
bar: bar
}
而现在如果我们的变量名与我们添加到对象中的属性名是一样的,我们就可以省略掉:变量名。
const bar = '123';
const obj = {
key: 'value',
bar
}
这两种方式实际上是完全等价的。除此之外如果我们需要为对象添加一个普通的方法,传统的做法是通过方法名:函数表达式,现在我们可以省略里面的:function, 这两种方式同样也是等价的。
const bar = '123';
const obj = {
key: 'value',
bar,
// method1: function () {
// console.log('method1');
// },
method1 () {
console.log('method1');
}
}
console.log(obj)
不过需要注意的是这种方法的背后他实际上就是普通的function,也就是说如果我们通过对象去调用这个方法,那么内部的this就会指向当前对象。
另外对象字面量还有一个很重要的变化就是,他可以使用表达式的返回值作为对象的属性名。以前如果说我们要为对象添加一个动态的属性名,我们就只能在对象创建过后,然后通过索引器的方式也就是[]
来去动态添加。
const obj = {};
obj[Math.random()] = 123;
在ES2015过后,对象字面量的属性名直接可以通过[]
直接去使用动态的值了,这样一个特性叫做计算属性名,具体的用法就是在我们属性名的位置用[]
包起来。在里面就可以使用任意的表达式了。这个表达式的执行结果将会作为这个对象的属性名。
const obj = {
[Math.random()]: 123,
}
Object.assign
ECMAScript中为Object对象提供了一些扩展方法,这里我们来看几个最主要的方法,首先是assign方法,这个方法可以将多个源对象当中的属性复制到一个目标对象当中,如果对象当中有相同的属性,那么我们源对象当中的属性就会覆盖掉目标对象的属性。
这里所说的源对象和目标对象他们都是普通的对象,只不过用处不同,我们是从源对象当中取,然后往目标对象当中放。
例如我们这里先定义一个source1对象,在这个对象当中我们定义一个a属性和一个b属性。然后我们再来定义一个target对象,这个对象当中我们也定义一个a属性,还有一个c属性。
const source1 = {
a: 123,
b: 123,
}
const target = {
a: 456,
c: 456
}
有了这两个对象之后我们就可以使用Object.assign方法去合并他们了,Object.assign支持传入任意个数的参数,其中第一个参数就是我们的目标对象,也就是说我们所有源对象当中的属性都会复制到目标对象当中。这个方法的返回值也就是这个目标对象。
const result = Object.assign(target, source1);
console.log(target, result === target); {a: 123, c: 456, b: 123 }// true
可以发现目标对象中的a被源对象覆盖掉了,c还是目标对象原本的,b是从源对象当中复制过来的。这就是assign的作用,总结一下就是用后面的对象覆盖目标对象的属性。
除此之外我们也能看到assign方法的返回值其实就是第一个对象,他俩是完全相等的。
如果我们要传入多个源对象,例如我们这里再来定义一个source2对象
const source2 = {
b: 789,
d: 789,
}
然后我们将source2也传入到Object.assign方法当中, 效果也是一样的,他只是依次的把我们每一个源对象当中的属性去覆盖到第一个对象当中。
const result = Object.assign(target, source1, source1);
这个方法其实特别常用。很多时候我们都可以使用他去复制一个对象。例如我们这里来定义一个函数。在这个函数中去接收一个对象参数。在这种情况下如果我们在这个函数内部直接修改了这个对象参数的属性,那外界这个对象也同时会发生变化。因为我们知道他们是指向同一个内存地址,也就是同一个数据。
function func (obj) {
obj.name = 'zd';
}
const obj = { name: 'yd' };
func(obj);
console.log(obj);
如果我们只是希望在这个函数内部去修改这个对象,我们就可以使用Object.assign方法,去把这个对象复制到全新的一个空对象当中,那这样的话我们内部的这个对象就是一个全新的对象。他的修改也就不会影响到外部的数据了。
function func (obj) {
const funcObj = Object.assign({}, obj);
funcObj.name = 'zd';
}
const obj = { name: 'yd' };
func(obj);
console.log(obj);
除此以外,Object.assign用来为options对象参数设置默认值也是一个非常常见的应用场景。
const default = {
name: 'yd',
age: 18
}
const options = Object.assign(default, opt);
Object.is
ECMAScript中还为Object对象新增了一个is方法用来判断两个值是否相等。在此之前我们再ECMAScript当中去判断两个值是否相等我们可以使用两个等号的相等运算符。或者是三个等号的严格相等运算符。
那这两者是区别是两等运算符会在比较之前自动转换数据类型,那也就会导致0 == false
这种情况是成立的。而三等他就是严格去对比两者之间的数值是否相同。因为0和false他们之间的类型不同所以说他们是不会严格相等的。
但是严格相等运算符他也有两个特殊情况,首先就是对于数字0,他的正负是没有办法区分的,也就是说在我们用三等运算符去比较+0和-0时,返回的结果是true。
当然这对我们应用开发来讲这种问题其实并不需要关心,只有在处理一些特殊的数学问题的时候才会有这种情况出现。
其次是对于NaN, 两个NaN在三等比较时是不相等的。以前认为NaN是一个非数字,也就是说他有无限种可能,所以两个NaN他是不相等的,但在今天看来,NaN他实际上就是一个特别的值,所以说两个NaN他应该是完全相等的。
所以说在ES2015中就提出了一种新的同值比较的算法,那可以使用Object对象全新的is方法来解决这个问题,通过Obejct.is正负零就可以被区分开,而且NaN也是等于NaN的。
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
不过一般情况下我们根本不会用到这个方法,大多时候我们还是使用严格相等运算符,也就是三个等号。
Proxy
如果我们想要监视某个对象中的属性读写,我们可以使用ES5中提供的Object.defineProperty这样的方法来去为我们的对象添加属性,这样的话我们就可以捕获到我们对象中属性的读写过程。
这种方法实际上运用的非常广泛,在Vue3.0以前的版本就是使用这样的一个方法来去实现的数据响应,从而完成双向数据绑定。
在ES2015当中全新设计了一个叫做Proxy类型,他就是专门为对象设置访问代理器的,那如果你不理解什么是代理可以想象成门卫,也就是说不管你进去那东西还是往里放东西都必须要经过这样一个代理。
通过Proxy就可以轻松监视到对象的读写过程,相比于defineProperty,Proxy他的功能要更为强大甚至使用起来也更为方便,那下面我们具体来看如何去使用Proxy。
这里我们定义一个person对象,我们通过new Proxy的方式来去为我们的person来创建一个代理对象。
Proxy构造函数的第一个参数就是我们需要代理的对象,这里是person,第二个参数也是一个对象,我们可以把这个对象称之为代理的处理对象,这个对象中可以通过get方法来去监视属性的访问,通过set方法来去介绍对象当中设置属性这样的一个过程。
const person = {
name: 'yd',
age: 18
}
const personProxy = new Proxy(person, {
get() {},
set() {}
})
我们先来看get方法,这个方法最简单可以接收两个参数,第一个就是所代理的目标对象,第二个就是外部所访问的这个属性的属性名。这个方法的返回值将会作为外部去访问这个属性得到的结果。
{
get(target, property) {
console.log(target, property);
return property in target ? target[property] : undefined;
}
}
我们再来看下set方法,这个方法默认接收三个参数, 分别是代理目标对象,以及我们要写入的属性名称还有最后我们要写入的属性值。
我们可以做一些校验,比如说如果设置的是age,他的值就必须是整数,否则就抛错。
{
set(target, property, value) {
console.log(target, property, value);
if (property === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError(``${value} must be a integer);
}
}
target[property] = value;
}
}
以上就是Proxy的一些基本用法,在以后Proxy会用的越来越多,Vue3.0开始就开始使用Proxy去实现内部的数据响应了。
Proxy 对比 defineProperty
了解了Proxy的基本用法过后接下来我们再深入探讨一下相比于Object.defineProperty, Proxy到底有哪些优势。
首先最明显的优势就是在于Proxy要更为强大一些,那这个强大具体体现在Object.defineProperty只能监听到对象属性的读取或者是写入,而Proxy除读写外还可以监听对象中属性的删除,对对象当中方法的调用等等。
这里我们为person对象定义一个Proxy对象,在Proxy对象的处理对象中的外的添加一个deleteProperty的代理方法,这个方法会在外部对当前这个代理对象进行delete操作时会自动执行。
这个方法同样接收两个参数,分别是代理目标对象和所要删除的这个属性的名称。
const person = {
name: 'yd',
age: 18
}
const personProxy = new Proxy(person, {
deleteProperty(target, property) {
console.log(target, property);
delete target[property];
},
})
这是Object.defineProperty无法做到的, 除了delete以外, 还有很多其他的对象操作都能够被监视到,列举如下。
get: 读取某个属性
set: 写入某个属性
has: in 操作符调用
deleteProperty: delete操作符调用
getProperty: Object.getPropertypeOf()
setProperty: Object.setProtoTypeOf()
isExtensible: Object.isExtensible()
preventExtensions: Object.preventExtensions()
getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor()
defineProperty: Object.defineProperty()
ownKeys: Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertSymbols()
apply: 调用一个函数
construct: 用new调用一个函数。
接下来我们来看第二点优势就是对于数组对象进行监视,
通常我们想要监视数组的变化,基本要依靠重写数组方法,这也是Vue的实现方式,proxy可以直接监视数组的变化。以往我们想要通过Object.defineProperty去监视数组的操作最常见的方式是重写数组的操作方法,这也是Vue.js中所使用的方式,大体的方式就是通过自定义的方法去覆盖掉数组原型对象上的push,shift之类的方法,以此来劫持对应的方法调用的过程。
我们这里来看如何直接使用Proxy对象来对数组进行监视。这里我们定义一个list数组,然后对这个list数组进行Proxy监视。
在这个Proxy对象的处理对象上我们去添加一个set方法,用于监视数据的写入,在这个方法的内部我们打印参数的值,然后再target对象上设置传入的值,最后返回一个true表示写入成功。
这样我们再外部对数组的写入都会被监视到,例如我们这里通过push向数组中添加值。
const list = [];
const listproxy = new Proxy(list, {
set(target, property, value) {
console.log(target, property, value);
target[property] = value;
return true; // 写入成功
}
});
listproxy.push(100);
Proxy内部会自动根据push操作推断出来他所处的下标,每次添加或者设置都会定位到对应的下标property。
数组其他的也谢操作方式都是类似的,我们这里就不再演示了。这就是Proxy对数组的一个监视。他的功能还是非常强大的,这一点如果我们放在Object.defineProperty上要想去实现的话就会特别的麻烦。
最后相比于Object.defineProperty还有一点优势就是,Proxy是以非入侵的方式监管了对象的读写,那也就是说一个已经定义好的对象我们不需要对对象本身去做任何的操作,就可以监视到他内部成员的读写,而defineProperty的方式就要求我们必须按特定的方式单独去定义对象当中那些被监视的属性。
对于一个已经存在的对象我们要想去监视他的属性我们需要做很多额外的操作。这个优势实际上需要有大量的使用然后在这个过程当中去慢慢的体会。
Reflect
Reflect是ECMAScript2015中提供的一个全新的内置对象,如果按照java或者c#这类语言的说法,Reflect属于一个静态类,也就是说他不能通过new的方式去构建一个实例对象。只能够去调用这个静态类中的静态方法。
这一点应该并不陌生,因为在javascript中的Math对象也是相同的,Reflect内部封装了一系列对对象的底层操作,具体一共提供了14个静态方法,其中有1个已经被废弃掉了,那还剩下13个,仔细去查看Reflect的文档你会发现这13个方法的方法名与Proxy的处理对象里面的方法成员是完全一致的。
其实这些方法就是Proxy处理对象那些方法内部的默认实现,你可能觉得这句话不是很好理解,我们这里来用代码说明一下。
这里我们定义一个proxy对象。只是proxy处理对象中什么也没有写,通过前面的介绍我们可以知道,我们可以在这个proxy处理对象中去添加不同的方法成员来去监听对象所对应的操作。
const obj = {
foo: '123',
bar: '456',
}
const proxy = new Proxy(obj, {
})
如果说我们没有添加具体的处理方法例如get或者set,那他内部这些get或者set是怎样执行的呢?其实proxy处理对象内部默认实现的逻辑就是调用了Reflect对象当中所对应的方法。
那也就是说,我们没有定义get方法就等同于是定义了一个get方法,在内部将参数原封不动的交给Reflect的get方法,结果是一样的。
const proxy = new Proxy(obj, {
get(target, property) {
return Reflect.get(target, property);
}
})
那这也就表明我们在实现自定义的get或者set这样的逻辑时更标准的做法是,先去实现自己所需要的监视逻辑,最后再去返回通过Reflect中对应的方法的一个调用结果。
const proxy = new Proxy(obj, {
get(target, property) {
console.log('实现监视逻辑');
return Reflect.get(target, property);
}
})
Reflect对象的用法其实很简单,mdn上实际上已经有了非常清晰的介绍,但是大多数人接触到这个对象的第一个感觉就是为什么要有Reflect这样一个对象。也就是说他的价值具体体现在什么地方。
个人认为Reflect对象最大的意义就是他提供了一套统一操作Object的API,因为在这之前我们去操作对象时有可能使用Object对象上的方法,也有可能使用像delete或者是in这样的操作符,这些对于新手来说实在是太乱了,并没有什么规律。
Reflect对象就很好的解决了这样一个问题,他统一了对象的操作方式,我们可以通过几个简单的例子来看一下。
这里我们先定义一个obj对象,然后在对象当中定义name和age。按照传统的方式如果我们需要判断这个对象中是否存在某个属性,我们需要使用in这个语句,用到in操作符,删除name属性我们需要使用到delete语句。而如果说我们需要获取对象中所有的属性名,那有需要去使用Object.keys这样的方法。
const obj = {
name: 'yd',
age: 18,
}
console.log('name' in obj);
console.log(delete obj['age']);
console.log(Object.keys(obj));
那也就是说我们同样都是去操作这个对象,但是我们一会需要用操作符的方式,一会又需要用到某一个对象当中的方法。
换做现在Reflect对象就提供了一个统一的方式,那我们去判断这个对象当中是否存在某一个属性我们可以使用Reflect.has方法。
console.log(Reflect.has(obj, 'name'));
删除一个属性我们可以使用deleteProperty方法。
console.log(Reflect.deleteProperty(obj, 'age'));
对于想要获取对象中所有的属性名我们可以使用ownKeys方法,那这样的一种体验也会更为合理一点,当然这只是个人的一个感悟,这个还是需要个人多多体会。
console.log(Reflect.ownKeys(obj));
需要注意的一点是,目前以前的那些对象的操作方式还是可以使用的,但是ECMAScript他希望经过一段时间的过渡过后以后的标准中就会把之前的那些方法把他给废弃掉。
所以我觉得我们现在就应该去了解这13个方法以及他们各自取代的用法。这些内容在mdn上都有完整的描述,我们这里就不重复了。