目录:
- 新增的对象字面量语法
- 成员速写
- 方法速写
- 计算属性名
- 新增的Object方法
- Object.is
- Object.assign
- Object.setPrototypeOf
- Object.keys, Object.values, Object.entries
- Class
- 【 扩展 】面向对象简介
- 用class(类)来实现更好的面向对象支持
- 类的其他书写方式
- 类的继承
新增的对象字面量语法
成员速写
ES5在书写对象成员的一些问题
我们在开发中总是会遇到这样的逻辑, 用户输入了用户名和密码, 我们拿到密码提交给后端
// 这个方法就是提交给后端校验的方法, 我们在取得用户名和密码以后要调用该方法
function loginHandler( username, pwd ) {
ajax('/login', {
username: username,
pwd: pwd
})
}
loginHandler( 'admin', 123 );
一般都是这么写的吧, 后端肯定是接收一个对象, 我们会发现有一个怪异的地方
- 在ajax请求中, username和pwd被书写了两次, 因为第一个username和pwd是key名, 第二个是value值, 这本无伤大雅, 但是看着总觉得怪怪的
ES6的解决方案
ES6允许我们当对象的属性value来自于一个变量, 且该变量名和对象属性key名一致, 则可以进行成员速写来减少代码量
function loginHandler( username, pwd ) {
ajax('/login', {
username, // 因为key和value名字一致, 所以我们直接省略了value, 这样写等同于username: username
pwd // 这样写等同于pwd: pwd
})
}
loginHandler( 'admin', 123 );
方法速写
过去我在对象中书写方法
在过去中, 我们在对象中书写方法如下
const obj = {
sayHello: function() {
console.log('hello');
}
}
ES6的简洁写法
ES6允许我们在对象字面量中书写方法时可以省略冒号和function关键字
const obj = {
sayHello() {
console.log('hello');
}
}
计算属性名
在过去我们处理对象成员名来自一个表达式的状况如下
const prop1 = 'name';
const obj = {}; // 如果一个对象的key来自于一个变量, 那么我们就不能在初始化的时候将他放进字面量中
obj[prop1] = 'loki'; // 只能在后续这样处理
console.log(obj); // { name: 'loki' }
这样写其实也没什么问题, 但是从逻辑上来说这有一点不恰当, 因为属性初始化是我在一开始就可以做到的, 但是ES6之前就做不到
ES6的解决方案
ES6允许我们直接给对象字面量的key值用表达式, 只需要加上中括号即可
const prop1 = 'name';
const obj = {
[prop1]: 'loki'
}
console.log(obj); // { name: 'loki' }
新增的Object方法
Object.is
用于判断两个数据是否相等
在过去我们判定两组数据相等的情况
ES6之前我们一般使用 == 做普通比较, 用 === 做严格比较, 但是在做严格比较的时候有一些问题
console.log( +0 === -0 ); // true
console.log( NaN === NaN ); // false
以上两点其实都不太合理, +0和-0其实一个为正一个为负, 虽然本质上来说他们没有太大的意义, 但是严格按照数学逻辑这两个值应该是不等于的, 第二个就更加离谱了, NaN竟然不等于NaN, 这就完全说不过去了吧
ES6的解决方案
所以ES6推出的 Object.is 帮助我们很好的解决了这个问题
// Object.is 的比较跟严格比较基本上一样, 除了以上笔者提到的两个 === 的问题, Object.is进行了修复
console.log( Object.is( +0, -0 ) ); // false
console.log( Object.is( NaN, NaN ) ); // true
Object.assgin
用于混合对象
过去我们遇到需要混合对象的情况
// 如果我们要将下面两个对象合并, 在ES6之前我们一般这么作
const obj1 = {
name: 'loki',
age: 18
}
const obj2 = {
name: 'thor',
age: 20
}
// 我们就来用用我们的收集运算符
function assign( ...objs ) {
let lastObj = {};
objs.forEach( it => {
for( let prop in it ) {
lastObj[prop] = it[prop];
}
} )
return lastObj;
}
const lastObj = assign( obj1, obj2 );
console.log(lastObj); // 输出 {name: "thor", age: 18, sex: "male"}
基本上我们都会自己封装一个方法来进行合并, 有时候会略显麻烦, 甚至其实我们自己写的方法可能性能不是很友好
ES6的解决方案
学习ES6以后, 我们之前有知道ES6可以使用扩展运算符进行对象的合并, 来回忆一下
const obj1 = {
name: 'loki',
age: 18
}
const obj2 = {
name: 'thor',
age: 20
}
const lastObj = {...obj1, ...obj2}; // 就这么一句就实现了对象的合并
console.log(lastObj); // 输出 {name: "thor", age: 18, sex: "male"}
同时ES6还提供了一个新的api, 也就是Object.assign, 供我们使用来合并对象
Object.assign不限定参数, 他会从最后一个参数开始依次将之前的参数进行合并最后覆盖掉第一个参数( 也就是说会改动第一个参数 ), 并将第一个参数的值返回, 说起来可能比较抽象, 但是一看你就会了
const obj1 = {
name: 'loki',
age: 12
}
const obj2 = {
name: 'thor',
age: 16,
sex: 'male'
}
const obj3 = {
skill: 'eat'
}
const lastObj = Object.assign( obj1, obj2, obj3 );
console.log(lastObj); // { name: 'thor', age: 16, sex: 'male', skill: 'eat' }
console.log(lastObj === obj1); // true
就是你传递进去的第一个参数最终是会被覆盖的, 你可以理解为第一个参数为target, 是你最终要丢出去的对象, 而后面的剩余参数都是origin, 是你要进行复制的源对象, 像上面那样操作会影响第一个对象的值, 所以我们一般是这样操作的
const obj1 = {
name: 'loki',
age: 12
}
const obj2 = {
name: 'thor',
age: 16,
sex: 'male'
}
const obj3 = {
skill: 'eat'
}
// 我们直接将第一个对象设置成一个新的对象, 这个也是我们最终要得到的对象, 他也不会对其他对象造成更改啦
const lastObj = Object.assign( {}, obj1, obj2, obj3 );
console.log(lastObj); // { name: 'thor', age: 16, sex: 'male', skill: 'eat' }
console.log(lastObj === obj1); // false
Object.setPrototypeOf
过去我们关于设置一个对象的原型
// 1. 使用Object.create
function Person() {}
const obj = Object.create(Person.prototype);
console.log(obj.__proto__ === Person.prototype); // true
// 2. 直接修改__proto__
const obj2 = {};
obj2.__proto__ = Person.prototype;
console.log(obj2);
第一种方案吧, 只能在创建的时候设置, 中途想改就走不了了, 有局限性, 第二种方案虽然是可以更改, 但是人家都说了__proto__是隐式的属性, 你非去抓着人家的隐式属性改不符合道理啊, 况且有些浏览器还不给你改这个属性
ES6的方案
Es6给我们推出了setPrototypeOf可以让我们随时随地的修改一个对象的原型
function Person() {}
const obj = {};
Object.setPrototypeOf(obj, Person.prototype);
console.log(obj.__proto__ === Person.prototype); // true
// 啥? 你问我创建的时候怎么办? Object.create是干啥吃的啊
Object.keys, Object.values, Object.entries
ES5我们取一个对象的属性的做法
const obj = {
name: 'loki',
age: 18
}
// 通常会自己封装一个函数
function getObjKeys( obj ) {
if( !obj instanceof Object ) {
return;
}
const keys = [];
for( const prop in obj ) {
keys.push( prop );
}
return keys;
}
const keys = getObjKeys( obj );
console.log(keys); // ['name', 'age']
这样可能略显麻烦, 于是…
ES6的解决方案
const obj = {
name: 'loki',
age: 18
}
const keys = Object.keys( obj );
console.log(keys); // ['name', 'age']
其实在 ES6中远不止提供了Object.keys, 它还提供了Object.values和Object.entries, 我们来看看他们各自都是处理啥的
// Object.keys是拿到对象的key的集合, 那么Object.values就是拿到对象的value集合
const obj = {
name: 'loki',
age: 18
}
const values = Object.values( obj ); // [ 'loki', 18 ]
// Object.entries就是拿到键值对的集合
const entries = Object.entries( obj ) // [['name', 'loki'], ['age', 18]]
Class
【 扩展 】面向对象简介
面向对象: 一种编程思想, 跟具体的语言无关
面向过程的思考点是功能步骤
面向对象的思考点是对象的划分
我们来看看知名例子【大象装冰箱】通过面向对象和面向过程分别是怎么处理的, 从中你应该能够发现一些这两种编程思维的差异
// 面向过程的大象装冰箱
// 1. 打开冰箱函数
function openRefrigeratorDoor() {
console.log('打开冰箱门');
}
// 2. 将大象塞进去
function pushElephantIn() {
console.log('将大象塞进去');
}
// 3. 关上冰箱门
function closeRefrigeratorDoor() {
console.log('关上冰箱门')
}
// 面向对象的大象装冰箱
function Elephant() {
}
Frigerator.prototype = {
openDoor() {
console.log('打开冰箱门');
},
closeDoor() {
console.log('关闭冰箱门');
},
push( target ) {
console.log(`将${ target }装进冰箱`);
}
}
function Frigerator() {
}
用class( 类 )来实现更好的面向对象支持
过去我们声明构造函数的方式
function Person( name, age ) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
Person.printA = function() {
console.log('a');
}
像上面这样声明构造函数, 其实是有如下几个弊端的
- 属性和原型方法定义分离, 降低了代码的可读性
- 原型成员可以被枚举
- 默认情况下, 构造函数依然可以当做普通函数调用
ES6声明构造函数的方式
class Person {
// 所有的静态资源都在属性或者函数之前加一个static关键字
static printA() {
console.log('a');
}
// 下面这个constructor就等同于上面的function Person() {}, 就等于是构造函数的核心
constructor( name, age ) {
this.name = name;
this.age = age;
}
// 所有的原型方法都写在这儿
sayName() {
console.log(this.name);
}
// 原型方法foo
foo() {}
}
ES6认为使用class类来定义一个构造函数更加的ok, 也让功能更加的集中了
类声明的特点:
- 类的声明不会被提升, 与let, const一样存在暂时性死区
- 类中的所有代码均在严格模式下运行
- 类的所有方法都是不可枚举的
- 类的所有方法在内部都不可以当做构造函数使用
- 类的构造器必须使用new运算符来调用
类的其他书写方式
可计算的成员名
// 我们的想把这个printName的变量值作为class类的方法名
const printName = 'print';
class Person {
constructor( name ) {
this.name = name;
}
// 直接利用ES6的可计算属性名
[ printName ]() {
console.log(this.name);
}
}
getter和setter
如果我们想定义一个对象某个属性的读取和写入( 捕捉到修改和读取的东西 ), 那么在过去我们一般用的都是Object.defineProperty, 在class中, ES6给我们提供了更加方便的getter和setter
// 比方说我们要设置constructor中用户传入年龄的getter和setter, 年龄不能大于200
// 因为一个人年龄大于20是不合理的
class Person {
constructor( name, age ) {
this.name = name;
this.age = age;
}
get age() {
console.log( 'hello' );
return this._age;
}
set age( age ) {
console.log('age被修改了');
if( age > 200 ) {
age = 200;
}
this._age = age;
}
}
const person = new Person( 'loki', 18 );
person.age = 300;
console.log(person.age); // 200
静态成员
在ES5中我们书写静态成员一般是如下这样
这样其实又将类的属性和类本身分离了不利于阅读, 所以ES6给我们推出了一种新的书写静态成员的写法
在class类中我们知道书写原型成员是直接在class中写就行, 那么书写静态成员也是一样, 唯一区分他们的就是静态成员你得加上
static
关键字
class Person {
constructor( name ) {
this.name = name;
}
// 原型方法sayName
sayName() {
console.log(this.name);
}
// 加上static 就是静态成员
static printMaxLife() {
console.log( '200' );
}
}
字段初始化器
在字段初始化器没出来之前, 我们是很难在class类上加静态属性的, 字段初始化器允许我们直接在class类中使用赋值号声明静态属性
class Person {
constructor( name ) {
this.name = name;
}
// 就是下面的写法, 直接用赋值号
static maxLifeTime = 200;
static = () => {
console.log(this);
}
}
字段初始化器来了以后, 他也允许我们在class中书写箭头函数了, 而之前是不行的, 在某些情况中, 可以帮助我们解决this的问题( 如果你有使用过react的话, 我相信你知道我在说什么 )
注意点:
- 使用static关键字的字段初始化器, 添加的是静态成员
- 没有使用static关键字的字段初始化器, 添加的成员不是原型成员, 而会直接添加到对象身上
class Person {
constructor( name ) {
this.name = name;
}
sayName = () => {
console.log(this.name);
}
}
const person1 = new Person( 'andy' );
const person2 = new Person( 'peggy' );
console.log(person1.sayName === person2.sayName); // false
【 扩展 】Decorator( 装饰器 )
比如我们有一个需求, 当我们调用类里面的某个方法的时候, 我们要进行日志的记录, 代表该方法被调用了, 很多同学可能会这么写
class Person {
constructor( name ) {
this.name = name;
}
sayName() {
console.log('方法被调用啦');
console.log(this.name);
}
}
上面这样写有如下几个问题:
- 你能否在sayName的输出方法被调用的那一行中记录是啥方法被调用了, 在ES5非严格模式你还可以用callee, 在严格模式下你能有办法吗?
- 假如class中有很多个方法都要进行日志的记录, 然后很多类中也有同样的记录日志的功能, 你还会这样吗?
针对于第二点很多朋友可能会立马想到函数, 对装饰器的本质也是一个函数, 我们来看使用装饰器的写法
class Person {
constructor( name ) {
this.name = name;
}
@printLog // 直接打上一个@标记后面跟你要装饰的函数名
sayName() {
console.log(this.name);
}
}
// target => 哪个类, methodsName => 哪个方法, descriptot => 方法的描述对象: 可枚举型可配置性等
function printLog( target, methodsName, descriptor ) {
console.log(`${target.name}类的${methodsName}方法被执行了`);
}
类的继承
如果两个类A和B, 如果可以描述为A是B, 则A和B形成继承关系( 比如狗是动物, 所以狗和动物形成继承关系 )
ES6之前JS的继承
最完美的就是圣杯模式了
// 圣杯模式
const inherit = (function () {
function F() { }
return function (target, origin) {
F.prototype = origin.prototype;
target.prototype = new F();
target.prototype.constructor = target;
target.prototype.super = origin;
}
}())
Animal.prototype = {
say() {
console.log('动物可以说话');
}
}
function Animal() {
}
function Dog() { }
inherit(Dog, Animal);
const dog = new Dog();
dog.say(); // 输出动物可以说话
console.log(dog.constructor, dog.super); // 输出function Dog() {} function Animal() {}
ES6的一些处理方式
- Object.setPrototypeOf
// 上面才写过的Object.setPrototypeOf
Animal.prototype = {
say() {
console.log('动物可以说话');
}
}
function Animal() {}
function Dog() {}
Object.setPrototypeOf( Dog.prototype, Animal.prototype );
const dog = new Dog();
dog.say(); // 输出动物可以说话
- Class的extends
上面的写法虽然OK, 但是又像我们之前说的一样, 类的一些操作跟类本身脱离开了, 显得不太好阅读, 于是es6给我们提供了extends继承
class Animal {
say() {
console.log('动物可以说话');
}
}
class Dog extends Animal {
}
const dog = new Dog();
dog.say(); // 输出动物可以说话
关于借用构造函数的那些事儿
我们知道在某种情况下, 我们继承了父类还不足以达到我们的要求, 我们甚至在构造函数中在this上创建的属性都高度的一致, 如下
function Animal( name, sex ) {
this.name = name;
this.sex = sex;
}
// 我们可以直接new 一个Animal, 也可以细分成Dog类如下
function Dog( name, sex ) {
// 这里我们也要写this.name = name, this.sex = sex, 太麻烦了, 所以我们会借用构造函数完成自己的功能, 如下
Animal.call( this, name, sex );
}
const dog = new Dog('旺财', '男');
console.log(dog.name, dog.sex); // 旺财, 男
在ES6中我们可以直接使用super关键字来完成这一步
class Animal {
constructor( name, sex ) {
this.name = name;
this.sex = sex;
}
}
class Dog extends Animal {
constructor( name, sex ) {
// 使用super关键字效果和Animal.call(this, name, sex)是一样的
super( name, sex );
this.voice = '旺旺旺'; // 子类特有的属性
}
}
const dog = new Dog('旺财', '男');
console.log(dog.name, dog.sex); // 旺财, 男
注意: es6要求, 如果定义了constructor, 并且该类是子类, 则必须在该类的constructor第一行手动调用父类的构造函数( 手动调用super )
super关键字的两个用途:
- 当做函数使用, 代表父类的构造函数
- 如果当做对象使用, 则表示父类的原型
class Animal {
constructor( name, sex ) {
this.name = name;
this.sex = sex;
}
print() {
console.log('姓名', this.name);
console.log('性别', this.sex);
}
}
class Dog extends Animal {
constructor( name, sex ) {
super( name, sex ); // 当做函数使用代表父类的构造函数
}
print() {
super.print(); // 当做对象使用代表父类的原型
}
}