目 录CONTENT

文章目录

ES6基础

Administrator
2020-07-24 / 0 评论 / 6 点赞 / 7396 阅读 / 51424 字

ECMAScript 6 基础

ECMAScript 6也叫ECMAScript 2015,它是2015年发布一个继ES5.1以后的一个版本,不建议直接在浏览器里使用,因为兼容性非常低

在学习ES6的过程当中,我们建议使用nodejs做为学习环境 ,因为nodeJS就可以兼容ES6

什么是NodeJS

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境 。

Node 是一个让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为与PHPPythonPerlRuby 等服务端语言平起平坐的脚本语言。 发布于2009年5月,由Ryan Dahl开发,实质是对Chrome V8引擎进行了封装。

  1. NodeJS指包含ECMAScript,没有DOM与BOM(所以,就不会有window对象,也不会有document对象)
  2. 普通的JS代码是运行在浏览器上面的,而NodeJS代码可以像其它的后台编程语言一样运行在PC电脑上面,只需要在当前的电脑上面去安装开发环境(与其它后台语言如Java一样,Java也需要去安装jdk)
  3. node的全局对象不是window,在node下面,它的全局对象是

Node中如何运行JS代码

在浏览器里在,如果要运行某一个JS文件里面的代码,我们可以直接通过<script> 标签导入到网页里面就可以了,但是在node里面不行

$ node 文件名.js
$ node 1.js
$ node server.js

如果当前的JS文件与我们要执行的DOS命令的路径不一致,这个时候就要注意把路径加上去,如下所示

1569306384891

扩展知识:

.代表当前文件夹
..代表上级文件夹

我们可以通过cd命令去切换文DOS命令所在的文件夹,cd的全称是change directory

# 切换 到0924lianxi这个目录 
$ cd 0924lianxi
# 切换 到上及目录
$ cd .. 

dir命令显示当前路径下面的所有文件夹与文件

1569306827212

在上面的图片里面,01.js是当前文件夹下面的文件,页0924lianxi则一个文件夹,因为它的前面有一个<DIR>这个标记,有这个标记都是文件夹


变量的定义

在之前的ES5里面,我们去定义变量的时候,使用的是关键字var来定义变量,var有以下几个特点

  1. 没有块级作用域(只在定义在function里面的变量才有作用域)

  2. 可以在定义之前调用

    console.log(a);
    var a=123;
    
  3. 可以重复定义

    var a=123;
    var a="hello";
    

上面的三个问题在ES6都解决了

在ES6里面定义变量多了两个关键字,一个是let ,一个const

通过let定义的变量

console.log(a);   //报错
let a=123;

let定义的变量是先定义后调用(先声明后调用)

{
    let a=123;   //let定义的变量形成了作用域
}
console.log(a);  //报错

通过let定义的变量,它有块级作用域,以花括号为块级作用域

在使用let去定义变量的时候,一定要注意下面这种情况

let a="yangbiao";
{
    console.log(a);
    let a;
}

这种情况,a会报错,提示a没有被定义

在使用let去定义的时候,无论使用是的全局变量还是局变量,都要在定义之后使用

let a="123";
console.log(a);

let a="hello world";   //语法报错,提示a已经被定义了
console.log(a); 

通过const来定义

通过const 定义的数据与之前let定义的数据保持一个特性,同时,定义好的数据在这里是不允许更改栈内存值

const只锁了栈里面的数据,而并不能锁到堆里面去,所以在下面这种情况下面,要注意

const a=123;
console.log(a);

a=456;  //报错   基本数据类型在栈里面,所以栈是不可更改数据的

但是下面这种情况是可以的

const person={
    name:"杨标",
    sex:"男"
}
person.sex="男神";
console.log(person);  //不会报错

这个时候,person存在栈里面的地址并没有发生改变,上面的代码只是改变了堆里面的数据,所以并不会报错。但是下面这种情况就不行

const person={
    name:"杨标",
    sex:"男"
}

person={
    name:"向继发",
    sex:"男"
}    //直接报错
console.log(person);

当person在进行第二赋值的时候,就会报错,因为这个时候改变了栈里面的地址值

1569309760930

在使用const定义的时候,切记不能这么定义

const a;   //此种方案不可取

像上面这种定义方式是不可行的,它不报错,但是没有实际的值,后期也不能改变,但是我们可以这么使用

const a={};  //此种方案可取

最后一种特殊情况

 const sayHelo=function(){
     console.log("hello world");
 }

 //它会报错  
 sayHelo=function(){
     console.log("你好");
 }
sayHello();

方法的每次定义都是重新赋值,通过const定义的方法是绝对不能够被覆盖的


解构

数组解构

解构是变量的另一种取值与赋值方式,它针对于对象与数组

let a=1;
let b=2;
let c=3;

上面的这种写法可以直接用解构写成下面的情况

let [a,b,c]=[1,2,3];

如果参数的名称与参数的个数实现了一一对应,则正好相等,但是如果不对应则要注意

let [a,b,c]=[1,2];  //这个时候a=1,b=2,c=undefined;
//---------上面的定法其实就相当于下面的定法
let a=1;
let b=2;
let c;
let [a,b,[c,d]]=[1,2,["hello","world"]];  //这种情况也可以通过解构来解出来

对象的解构

对象的解构与数组的解构原理相同

let stu={
    userName:"杨标",
    sex:"男",
    age:18
}

//如果我现在想取得用户名,怎么办
/*
let userName = stu.userName;
let sex=stu.sex;
let age=stu.age;
*/
let {age,userName}=stu;    //相当于在对象里面拿出age属性值与userName的属性值
console.log(age,userName);

但是要注意,这里的变量名一定要与属性名相同,同时解构还可以像下面这种情况一下,进行二次解构

let stu={
    userName:"杨标",
    sex:"男",
    age:18,
    hobby:["看书","睡觉","玩游戏"]
}

let {hobby:[a,b]}=stu;

console.log(a,b);   // a就是看书,b就是睡觉

刚刚上面的操作都是把对象里面的东西拿出来,现在我们试着去反向操作


let userName="张三";
let age=18;
let sex="男";

/*
let stu={
    userName:userName,
    sex:sex,
    age:age
}
*/

let stu={userName,sex,age};

console.log(stu);

我们在创建对象的过程当中,如果发现属性名与属性变量值相同,这个时候,可以像上面这种方式一样,去直接解构赋值。如果你希望属性名与属性值在这里不相同,这个时候,我们可以按下面的方式 去操作

let userName="张三";
let age=18;
let sex="男";

/*
let stu={
   userName:userName,
   sex:sex,
   age:age
}
*/

let stu={
   stuName:userName,   //直接更换属性名
   sex,
   age
};

console.log(stu)

解构只与赋值方式有关,与前面的关键字没有关系 ,用let也可以,用var也行

函数的扩展

ES6对方法的书写方法,this指向以及参数等都做了一些扩展

箭头函数

箭头函数有时候也叫lamdba表达式,它是JS里面函数书写的另一种方式,现在我们来看一下箭头函数的书写更改

箭头函数的语法格式

没有参数的箭头函数

 /*
 setTimeout(function(){
    console.log("hello world");
 },2000);
*/

setTimeout(()=>{
    console.log("hello world");
},2000);

在上面的代码里面,我们可以看到,后面的箭头函数去掉了关键字function, 在括号与代码体之间使用=>相连。上面的例子我们都没有使用参数。现在我们来看有参数的箭头函数

有参数的箭头函数

/*
setTimeout(function(a,b){
    console.log(a,b);
},1000,"hello","world");
*/


setTimeout((a,b) => {
    console.log(a,b);
}, 1000,"hello","world");

有参数的箭头函数与没有参数的箭头函数是一样的,把参数写在这前function后面的扩号里面,如果是一个参数的时候,扩号可以省略

//这个箭头函数里面的参数只有一个,所以我们可以把扩号省略掉
setTimeout(userName => {
    console.log(userName);
}, 2000,"杨标");
定义普通的箭头函数

我们之前在使用箭头函数的时候,都是在setTimeout的回调函数里面(回调函数使用箭头函数是非常频繁),如果现在只是定义一个普通的方法,是否也可以使用箭头函数呢?

function sayHello(userName){
    console.log("大家好,我叫:"+userName);
}

上面的普通函数能否通过箭头函数去定义呢?

const sayHelo=userName=>{
    console.log("大家好,我叫"+userName)
}
sayHelo("杨标");

//下面是两个参数的定义
const sayHello2=(userName,sex)=>{
     console.log("大家好,我叫"+userName+",我的性别是"+sex);
 }
//下面是没有参数的定义
const sayHello3=()=>{
     console.log("hello world");
 }

如果方法体里面只有一行代码,这个时候,我们还可以省略掉花括号

const sayHello4 = () => console.log("hello world");
const sayHello2 = (userName,sex) => console.log("大家好,我叫"+userName+",我的性别是"+sex);
const sayHello = userName => console.log("大家好,我叫"+userName);
箭头函数与普通函数区别

箭头函数除了书写与定义方式与普通函数不一样以外,还有关键的一个点需要注意就是this指向性的问题。特别是对象里面的this指向与回调函数里面的this指向

/*
 var obj={
     userName:"杨标",
     sayHello:function(){
         //bind会生成一个新的方法
        var newfn=function(){
            console.log(this.userName)
        }.bind(this);

         setTimeout(newfn,1000)
     }
 }
 */
 //官方说明:箭头函数会绑定this指向,把箭头函数外部的this绑定到箭头函数里面去
 var obj={
     userName:"杨标",
     sayHello:function(){
        setTimeout(()=>{
            console.log(this.userName);
        },1000)
     }
 }
 obj.sayHello();

箭头函数会绑定this的指向,它会将箭头函数外部的 this绑定到箭头函数里边去(可以反向理解:跳过当前,取上级)。

我们在定义对象的时候,一定要注意,对象里面不允许定义箭头函数的属性。下面的情况,我们就拿不到当前对象的 this

 var obj={
     userName:"杨标",
     //这里将属性方法sayHello定义成了箭头函数 ,这个时候sayHello的this拿不到当前对象
     sayHello:()=>{  
         console.log(this.userName);    //undefined
        setTimeout(()=>{
            console.log(this.userName);  // undefined
        },1000)
     }
 }
 obj.sayHello();

函数参数的默认值

当我们在定义方法的时候,可能需要使用参数,在定义参数的时候可以给它一个默认值,这些在之ES5里面,我们需要在方法体里面处理一次,现在则不用了

ES5里面

const a=(userName,sex,age)=>{
    //打印三个变量,
    //如果age没有传值 ,则age默认的值为18
   age=age||18;
    
    console.log(userName,sex,age);

}

a("杨标","男",40);
a("杨标","男");   //这个时候没有传值,age就会使用方法体里面设置的默认值18

ES6里面

const b=(userName,sex,age=18)=>{
    console.log(userName,sex,age);
}

b("张三","女");  //没有传入参数,这个时候age就会使用定义的时候设置的默认值18
b("张三","女",22); //这个时候传入了具体的值,所以它是age为22

如果有多个参数的默认值,也是可以的

const fn=(z,x=1,y=2)=>{
    console.log(z,x,y);
}
fn(10);   //10,1,2
fn();    //undefined,1,2
fn(20,30); //20,30,2

注意:默认值,我们要放在参数的最后

数组的扩展

ES在数组对象里面扩展了一些新的方法,这些新的方法,我们挑一些常用的来列举一下

  1. Array.from()

    我们在ES5里面,如果要将一个类数组转换成数组,则需要使用Arary.prototype.slice.call()这种方法来进行数组的转换,在ES6里面多了一个新的方法来进行转换,这就是Array.from(),它可以将类数组转换成数组

    
     function abc(){
         console.log(Array.isArray(arguments));
         var arr=Array.from(arguments);
         console.log(Array.isArray(arr));
     }
     abc("a","b","c","d");
    
  2. Array.of()

    该方法用于创建一个数组

    在ES5里面,我们创建数组有两种情况

    var arr=new Array();
    var arr=[];
    

    当我们在创建数组的时候,会出现直接赋值的情况

    var arr=new Array(6);
    //这个时候创建了个长度为6的数组
    var arr=new Array("a");
    //这个时候创建了一个数组,里面的第一个元素是"a"ss
    

    Array.of(...items:[])这个方法返回的仍然是一个数组

     var arr = Array.of(6);  //这是创建了一个数组,里面的元素是6
     console.log(arr);
    

    这个方法得到的返回值是一个数组,而它的参数仍然可以是类数且的展开,所以我们也可以通过这一种方式把它转换成数组

    function abc(){
        console.log(Array.isArray(arguments));   //false
        // var arr=Array.from(arguments);
        var arr=Array.of(...arguments);
        console.log(Array.isArray(arr));   //true
    }
    abc("a","b","c","d");
    
  3. Array.ptototype.fill()

    这个方法是将数组填充成指定的元素

    var arr=new Array(10);
    arr.fill("h");
    console.log(arr);   //这会得到10个“h”的元素
    arr.fill("h",2,5);  //2代表开始索引  5代表结束索引的前一个
    arr.fill("s",2,-1); //2代表开始索引 ,-1代表倒数第一个(不包含)
    
  4. Array.prototype.find()

    在数组里面查找指定条件的元素,找到以后就返回这个结果,如果找不到就返回undefined,如果有多个找到到第一个就停止了

    let result=cityList.find((item,index,a)=>{
        return item.nm=="深圳";
    })
    console.log(result);
    

    通过find去找的时候,不要试图去更改你更有在遍历的这个数组,不然会影响结果

  5. Array.prototype.findIndex()

    在数组里面去找根据条件去找索引,找到以后就返回索引 ,找不到就返回-1

    let result = cityList.findIndex((item,index,a)=>{
        return item.id==20;
    })
    console.log(result);
    

Set单值集合

Set它是一个类数组,查是它是一个不重复的类数组,在里面,它不允许出现重复的元素(以前的时候,我们听说过键值对,但是Set只有值,没有键)

Set的初始化

首先,我们要学生会创建一个Set

let s=new Set();  //空的集合
let s=new Set(["a","b","c","d","e"]);  //集合里面存放了5个元素

Set是一个不重复的单值集合,所以当我们向里面添加重复的元素的时候,它会自动过滤掉

let s1=new Set(["a","b","c","d","e","a","c"]);

这个时候得到的结果仍然是Set { 'a', 'b', 'c', 'd', 'e' },它将重复的元素直接过滤掉了,所以我们可以使用这一个特性直接数组去重

let arr = ["a", "b", "c", "d", "e", "a", "c"];
let s=new Set(arr);
var arr1=Array.from(s);    
var arr2=Array.of(...s);
console.log(arr2);

这个时候,我们可以使用刚刚学过的转换数组的方法去将数Set转换成数组

Set对象的常用方法

  1. add()向集合里面添加元素,重复的元素添加不进去,它没有限定数据类型 ,它返回是当前添加进去以后的这个Set对象(它返回了自己,所以可以形成一个链式语法)
  2. delete()向集合里面删除元素,如果删除成功,则返回true,如果删除失败,则返回false
  3. has()判断当前Set集合里面有没有这个元素
  4. size属性,获取当前Set集合里面的元素个数

Set对象的遍历

首先我们使用for in遍历了一次,这不能用为什么?,因为for...in只能遍历有索引的集合(有索引的集合我们叫有序集合)

for(let i in arr){
    //i代表提索引
}

Set是一个单值集合,它没有键(也就没有索引),所以不能使用for..in去遍历,但是我们可以使用迭代器(Iterable)去遍历

let s=new Set();
s.add("a");
s.add("b");
s.add("c");
s.add(123).add(456);
for(var item of s){
    console.log(item);
}

Map键值对集合

Map与Set的数据类相似,它们都是一个集合, 但是map是一个键值对集合,它可以在添加的元素的过程当中,实现键与值的对象

Map的初始化

let m=new Map();  //创建了一个空的map集合

上面的代码是真初始化了一个空的map集合,我们也可以向构造函数Map里面传递参数,直接初始化

let obj={
    userName:"杨标",
    sex:"男"
}
var entry = Object.entries(obj);
let m=new Map(entry);

除了使用上面的方式来进行初始化以外,还可以使用下面的方式

let m=new Map([["userName","杨标"],["sex","男"],["age",18]]);

通过对map的初始化,我们可以知道它与Set的区别在于,它在添加元素的时候,有两个参数,一个是键,一个是值。map与对象非常相似,对象也叫键值对,map也是键值对,它们都是一个key对应一个value

Map对象的常用方法

  • set()方法,向map集合里面添加键值对,但是要注意,键不允许重复,如果重复则添加不进去,会把前面的值把它替换掉。添加完成以后返回的就是这个map,所以它可以实现链式语法
  • delete()方法,通过一个key向map集合里面删除一个键值对,删除成功返回true,删除失败返回false
  • has()方法,判断map集合里有没有这一个键
  • size属性,判断map里面键值对长度

Map对象的遍历

map是一个键值对集合,所以通过情况下会有三种需求的遍历

  1. 遍历所有的key
  2. 遍历所有的value
  3. 遍历所有的key与value
let m=new Map();
m.set("userName","杨标");
m.set("sex","男").set("age",18);
//---------
let keyIterator = m.keys();   //返回一个key的迭代器
while((abc=keyIterator.next()).done==false){
    console.log(abc.value);
}
//-----------
let valueIterator=m.values();
while((abc=valueIterator.next()).done==false){
    console.log(abc.value);
}

上面的遍历方式是针对key与value的两种不同方式,但是我们仍然可以使用我们后面学过的迭代器遍历方法for...of来进行

/*  第一步
for(let item of m){
    // console.log(item);
    console.log(item[0],item[1]);
}*/

/* 第二步 演变 
for(let item of m){
    let [key,value]=item;
    console.log(key,value);
}
*/
//第三步 演变
for(let [key,value] of m){
    console.log(key,value);
}

对象的扩展

之前我们已经对于对象的解构进行了扩展,现在我们来对对象的其它地方做一次扩展

对于对象方法的扩展

在以前创建对象里面的属性方法的时候,我们可以通过 function来进行

let a={
    sayHello:function(){
        console.log("你好");
    }
}
//在ES6时面,创建属性方法就更加简便了
let b={
    //通过这种方式定义的,就一定是一个属性方法,不能用构造方法的形式调用
     sayHello(){
         console.log("你好啊,这是新方法");
     }
 }

注意: 在第二种方式里面,这一种方式创建的属性方法它没有构造器(不能当成构造函数使用)

new a.sayHello();  //这是没有问题的,把 sayHello当成了构造函数使用

new b.sayHello();  //这个就会报错,提示 b.sayHello is not a constructor,不是一个构造函数

对访问器属性的扩展

在之前的ES5里面,我们可以通过Object.defineProperty 来定义对象的访问器属性

let stu={
    firstName:"杨",
    lastName:"标"
}
//访问器属性可以在get与set的方法里面实现联动效果
Object.defineProperty(stu,"userName",{
    //定义了get的取值方法 
    get:function(){
        return this.firstName+this.lastName;
    },
    //定义了set的赋值方法
    set:function(newValue){
        this.firstName=newValue[0];
        this.lastName=newValue.substr(1);
    }   
});
console.log(stu.userName);   //这里是取值,会自动调用get方法
stu.userName="张三丰";		   //赋值,会自动调用set方法
console.log(stu.firstName,stu.lastName);

在ES6里面,对访问器属性又重新做了一次规划

首先,我们可以模拟其它的编程语言一样(如Java),去写一个getset的方法

let stu={
    firstName:"杨",
    lastName:"标",
    //这个方法,我们把它当成取值的方法
    getUserName(){
        return this.firstName+this.lastName;
    },
    //下面这个方法,我们把它当成赋值方法
    setUserName(newValue){
        this.firstName=newValue[0];
        this.lastName=newValue.substr(1);
    }
}
console.log(stu.getUserName());
stu.setUserName("张三丰");
console.log(stu.firstName,stu.lastName);

但是这个时候要注意,它们的getUserName()setUserName()它们都是方法,不是属性。这个时候ES6是这么做的

let stu={
    firstName:"杨",
    lastName:"标",
    //这个方法,我们把它当成取值的方法
    get userName(){
        console.log("这是取值");
        return this.firstName+this.lastName;
    },
    //下面这个方法,我们把它当成赋值方法
    set userName(newValue){
        this.firstName=newValue[0];
        this.lastName=newValue.substr(1);
    }
}
console.log(stu.userName);
stu.userName="张三丰";
console.log(stu.firstName,stu.lastName);

ES6的访问器属性本质上面还是一个方法,它在取值操作的方法前面添加了一个get关键字,在赋值操作的方法前面添加了一个set关键字。在使用访问器属性的时候,就不要把它当成方法了,当成一个普通的属性使用就可以了

ES6对象新特性

对象的创建

在ES5里面,我们可以使用下面三种方式创建对象

  • var obj=new Object()
  • var obj={}
  • new一个构造函数

在ES6里面多了几种创建对象的方法,其中最主要的是通过关键字class来进行创建

在ES5里面,如果我们要使用构造函数去创建对象,那么,我们必须要先定义一个构造函数

function Person(name,sex,age){
    this.name=name;
    this.sex=sex;
    this.age=age;
    this.sayHello=function(){
        console.log(this.name,this.sex,this.age);
    }
}

let p=new Person("杨标","男",18);
console.log(p);

在上面的方法里面,我们创建了一个构造函数,现Person就是这个构造函数

在ES6里面,我们则可以使用另一种方式创建

class Person{
    //class 创建的构造函数里面,一定有一个方法constructor  你如果不写,它默认给你生成一个
    //如果不向里面传值 ,则可以不写
    constructor(name,sex,age){
        this.name=name;
        this.sex=sex;
        this.age=age;
    }
    sayHello(){
        console.log("hello world");
    }
}

let p=new Person("杨标","男",18);  //new的是Person,但是执行是constructor()构造方法
console.log(p);

我们通过class关键字来创建了一个构造函数,要注意,这个class创建的这个Person它通过typeof去检测的时候得到的是一个function,但是这个方法只能通过关键字new去调用

class创建构造函数Person里面,默认会有一个方法constructor() 这个方法,你可以自己写,但是你也不写(如果不写,系统会自动给你生成。当我们不需要向对象里面传递参数的时候,我们就可以省略掉)

constructor() 方法会在new的一瞬间自已调用,不需要用户手动去调用

对象的继承

在之前的ES5里面,我们可以通过prototype__proto__去实现对象的继承关系 ,但是它们各有优缺点

通过prototype实现

/**
 * 在以前的ES5里面
 */

function Person(name, sex, age) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.sayHello = function () {
        console.log(this.name, this.sex, this.age);
    }
}


function Student(){
    this.study=function(){
        console.log("学生在学习")
    }
}
//如果要让这个学生继承自人

Student.prototype=new Person();  //这是通过prototype来继承

let s=new Student();   //得到一个s对象

console.log(s instanceof Student);   //true
console.log(s instanceof Person);    //true

这种方式,可以实现继承,但是有个非常大的缺点,就是它不能够向父级对象传递参数,同时它也有一个最大优点就是保留了原型链的特点,我们通过 instanceof去检测的时候,无论是检测父级还是检测子级,结果都为true

通过__proto__实现继承

function Person(name, sex, age) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.sayHello = function () {
        console.log(this.name, this.sex, this.age);
    }
}

function Student(name,sex,age){
    this.__proto__=new Person(name,sex,age);
    this.study=function(){
        console.log("学生在学习")
    }
}
let s=new Student("杨标","男",19);
//这种方式解决了我们的参数不能传递的问题    但是又出现了一个新的问题
console.log(s instanceof Student);   //false
console.log(s instanceof Person);    //true

通过这种方式,我们可以实现继承的时候传递参数,但是我们的原型又被破坏了,当我们通过instanceof去检测的时候,我们发现这个时候检测父级为true,而检测当前对象又为false

ES5里面这两种情况都没有很好的去解决对象继承的问题。但是在ES6里面,则已经通过 class关键字与另一个继承关键字extends实现了

在ES6里面,如果要实现继承,需要使用新的关键字extends

/**人的对象的构造函数 */
class Person{
    constructor(name,sex,age){
        this.name=name;
        this.sex=sex;
        this.age=age;
    }
    sayHello(){
        console.log(this.name,this.sex,this.age);
    }
}
/**学生对象Student通过extends继承了Person */
class Student extends Person{
    constructor(name,sex,age,sid){
        super(name,sex,age);
        this.sid=sid;
    }
    study(){
        console.log("我在学习");
    }
}
let s=new Student("杨标","男",18,"20190101");
console.log(s);
console.log(s instanceof Student);
console.log(s instanceof Person);

注意事项:在使用 extends关键字实现继承的时候,我们要注意里面有这些注意点

  1. 在子类可以不写constructor()构造函数,系统会默认帮你创建
  2. 但是如果在子类写了constructor() 以后,一定要先调用super(),否则会报错
  3. 在构造函数里面,this 关键字必须在super之后

通过这一种方式实现的继承,它可以解决之前ES5里面存在的问题(要么不能传值,要么破坏原型链);

子级对象方法重写

当父级对象的方法无法再满足子级对象的时候,我们可以在子级对象里面重写这个方法,只要方法名相同的就行了。因为在JS里面,调用的时候都是先看自己有没有,如果自己有就不再去父级去找了

/**人的对象的构造函数 */
class Person{
    constructor(name,sex,age){
        this.name=name;
        this.sex=sex;
        this.age=age;
    }
    sayHello(){
        console.log(this.name,this.sex,this.age);
    }
}
/**学生对象Student通过extends继承了Person */
class Student extends Person{
    constructor(name,sex,age,sid){
        super(name,sex,age);
        this.sid=sid;
    }
    study(){
        console.log("我在学习");
    }
    sayHello(){
        console.log("我是子及元素");
        super.sayHello();   //在这里又通过关键字super去调用了父级元素的这个方法
    }
}
let s=new Student("张三","男",22);
//当子级元素有的时候,不找父级元素要
s.sayHello();   //这个时候调用的就是子级元素的方法

静态方法

ES6可以像其它的编程语言一样,实现静态方法了,只需要借助关键字static

class Person{
    constructor(){
        this.userName="张三";
    }
    sayHello(){
        console.log("我在向你打招呼",this.userName);
    }
    static sleep(){
        console.log("我在睡觉",this.userName);
    }
}
class Student extends Person{
    static sleep(){
        console.log("学生在睡觉");
    }
}
Person.sleep();
Student.sleep();

说明

  1. 静态就去不需要new就可以通过class后面的构造函数名直接调用,如Person.sleep()
  2. 静态方法可以实现继承
  3. 静态方法可以实现重写

在其它的编程语言里面,对象里面的静态方法是不能够被继承的,也不能被重写

String字符串的扩展

在之前,我们在进行字符串对象的学习的时候,我们已经学过了字符串的相应的方法,其中有些方法它本来就是ES6里面的方法,只是在浏览器里面它的兼容性非常好,所以我们直接使用

  1. padStart()
  2. padEnd();
  3. includes();
  4. startsWith();
  5. endsWith();

模板字符串

在之前学习字符的时候我们使用单引号或双引号来进行定符的定义

let str="hello world";
let str1='hello world';

上面的两种定义方式都是普通的字符串,在ES6里面多了一个新的字符串字义方式,使用反引号来完成

let str2=`hello world`;

通过这一种方式定义的字符串我们叫模板字符串,在模板字符串里面,我们可以很方便的去拼接我们的变量

let userName="杨标",sex="男",age=18;

如果按照普通的ES5的方式去拼接会很麻烦

let str="大家好,我的姓名是:"+userName+",我的性别是:"+sex+",我的年龄是:"+age;
let str="大家好,我的姓名是:".concat(userName).concat(",我的性别是:").concat(sex).concat(",我的年龄是:").concat(age);

但是在ES6里面,则很简单

let str=`大家好,我的姓名是:${userName},我的性别是:${sex},我的年龄是:${age}"`;

如果我们要在模板字符串里面去拼接变量,这个时候我们只需要在拼接的地方给一个 ${变量名}就可以了

同时模板字符串什么叫模板字符串,是因为保留了模板的功能

let str=`大家好!
    我的姓名是${userName},
    我的性别是${sex},
    我的年龄是${age}!
                 ${new Date().toLocaleString()}
                         杨标`;
console.log(str);

如果我们将字符串通过上面的方式去书定,这个时候打印的str也是按照这样的格式输出打印的,它也会换行,也面也会有空格

正则表达式

在JS里面,有一个对象比较特殊,它即不通过object创建,也不通过花括号创建,它直接就可以定义出来,在ES5与ES6当中都有一个构造函数RegExp,它是正则表达式的构造函数

let reg=new RegExp();   
//typeof reg;  得到object 它是一个对象

它仍然有另一种快速的构建方式

let reg=/ /;

正则表达式到底是干什么用的呢?

正则表达式是对数据进行快速验证的一种方式,它通过一些特殊的组合来对数据进行验证

一、元字符

元字符对应说明
.匹配除换行符之外的任意字符
\w匹配字母数字下划线,等同于:[a-zA-Z0-9_]
\s匹配任意空白符
\d匹配数字,等同于[0-9]
\b匹配单词边界
|或匹配,如 /x|y/ 正则可匹配x或y两个字符
^匹配字符串的开始
$匹配字符串的结束

二、反义字符

反义字符对应说明
[^x]匹配除“x”之外的所有字符,其中“x”可以为任意字符
[^xyz]同上,匹配除“x、y、z”之外的任意字符
\W匹配除了字母、数字、下划线之外的所有字符,等同于:[^\w]
\S匹配除空白符之外的任意字符,等同于:[^\s]
\B匹配不是单词边界的字符,等同于:[^\b]
\D匹配不是数字的所有字符,等同于:[^\d]

三、转义字符

转义字符对应说明
\xnn匹配十六进制数
\f匹配换页符,等同于:\x0c
\n匹配换行符,等同于:\x0a
\r匹配回车符,等同于:\x0d
\t匹配水平制表符,等同于:\x09
\v匹配垂直制表符,等同于:\x0b
\unnnn匹配Unicode字符,如:\u00A0

四、重复匹配

匹配字符对应说明
*重复出现零次或多次
+重复出现一次或多次
重复出现零次或一次
重复出现n次
{n,}至少重复出现n次
{m,n}重复重现m到n次,其中,m<n

五、修饰符

修饰符对应说明
iignoreCase的缩写,表示忽略字母的大小写
mmultiline的缩写,更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,
而不仅仅在整个字符串的开头和结尾匹配。
(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
gglobal的缩写,进行全局匹配,即对字符串进行全文匹配,直到字符串遍历结束
  • test()方法,用于测试某一个字符串是否满足正则表达式条件,如果满足就返回true,否则就返回false
  • exec()方法,用于提取字符串中满足的条件的字符串,要注意,它是一次提取一个,我们要通过while循环去提取,直到提取不到为止

正则表达式提取歌词

 var str=`[00:00.50]不谓侠(Cover 萧忆情Alex) - Critty
[00:01.50]词:迟意
[00:02.50]曲:潮汐-tide
[00:03.50]编曲:潮汐-tide
[00:30.80]衣襟上
[00:31.71]别好了晚霞
[00:34.16]余晖送我牵匹老马
[00:38.14]正路过
[00:39.25]烟村里人家
[00:41.40]恰似当年故里正飞花
[00:44.99]醉过风
[00:46.68]渴过茶
[00:48.59]寻常巷口寻个酒家
[00:52.44]在座皆算老友
[00:56.10]碗底便是天涯
`
//编写时间的正则表达式
var reg=/\d{2}:\d{2}.\d{2}/g;
//获取时间以后的数组
var timesArray=[]; 
//使用正则表达式方法exec去提取满足条件的字符串
while((item=reg.exec(str))){
   timesArray.push(item[0]);
}
//使用字符串方法replace来替换,获取时间以后的文字
var lrcArray = str.replace(reg,"").replace(/\[\]/g,",").split(",");
lrcArray.shift();

在使用正则表达式的时候,我们还有一个方法是字符串对象里面的方法,它也可以使用正则表达式,那就是replace,同时字符串里面还有一个方法match()也是使用正则表达式做为参数,用于检测匹配正则表达式的字符串,然后提取出来,放在一个数组里面,这样刚刚上面的代码我们就使用下面的方式进行简化

/*
var timesArray=[]; 
while((item=reg.exec(str))){
   timesArray.push(item[0]);
}
*/

这是原来的方式 ,使用正则对象的exec() 方法去提取,但是它一次只能提取一个,所以我们要使用 while 去循环

这个时候,当我们使用字符串对象里面的match() 方法的时候,我们就可以直接一次性获取了

var timesArray=str.match(reg);

Symbol数据类型

在JS里面已经有5种基本数据类型 ,分别是Number, String,Boolean,Null,Undefined这五种,在ES6里面,添加了一种新的基本数据类型叫Symbol,理解为独一无二的

Symbol理解为独一无二,通过调用Symbol() 的方法,返回一个变量,这个变量变是Symbol的数据类型

//0x0001
 let s1=Symbol();   //得到一个Symbol数据类型 

 //0x0002
 let s2=Symbol();   //又一个

 //每次调用Symbol()方法所得到的数据值都不一样
 //可以理解为Symbol()返回的是一个内存地址,每一个内存地址都不一样

 console.log(s1==s2);	

为什么需要一个这样的数据类型?

它是一个独一无二的东西,可以使用它做为对象的属性名。在ES5里面,所有的对象的属性名都是string类型 ,既然是字符串类型所以就有可能会被出现重复的情况被覆盖。但是有了symbol这个东西以后就不会了

在ES5里面

let obj1={
     userName:"杨标",
     sex:"男"
 }

 let obj2={
     userName:"张三丰",
     age:18
 }

 //obj1是目标对象。obj2是源对象
 Object.assign(obj1,obj2);
 console.log(obj1);

在上面的代码里面,我们可以看到obj1与obj2在这里它们都有一个共同的属性名叫userName,当我们通过Object.assign()去进行浅拷贝 ,把obj2拷贝到obj1里面去的时候,这个时候就发现obj1的属性userName被覆盖了

在ES6里面

let s1 = Symbol(); //独一无二的,不可能重复
let obj1 = {
    age: 18,
    [s1]: "杨标" //symbol做为属性名的时候,要使用[]括起来
};

{
    let s1=Symbol();   //独一无二的,不可能重复的
    let obj2 = {
        sex: "男",
        [s1]:"张三丰"
    }
    Object.assign(obj1,obj2);
    console.log(obj1);
}

我们把不希望被覆盖的属性使用symbol 来表示,Symbol做为属性名的时候,要使用[]包含起来

1569462623168

在上面的图片面,我们可以看到里面有两个Symbol,但是这两个Symbol属性不好区别,所以我们在创建Symbol的数据类型的时候,可以给它一个描述 ,这个描述相当于一个注释 ,不带任何实际意义

let s1 = Symbol("第一次"); //独一无二的,不可能重复
let obj1 = {
    age: 18,
    [s1]: "杨标" //symbol做为属性名的时候,要使用[]括起来
};
{
    // symbol里面的参数也没有实际意思,它做一个描述信息,相当于一段注释
    let s1=Symbol("第二次");   //独一无二的,不可能重复的
    let obj2 = {
        sex: "男",
        [s1]:"张三丰"
    }
    Object.assign(obj1,obj2);
    console.log(obj1);
}

1569462739414

Symbol属性不参于for...in的遍历。如果要获取这个属性名,需要使用特殊的方法Object.getOwnPropertySymbols(obj1)

let sarr = Object.getOwnPropertySymbols(obj1);
console.log(sarr);

获取到的sarr的结果为[ Symbol(第一次), Symbol(第二次) ]

Symbol的属性不能使用.来进行调用,要使用[] 来进行调用,但是在调用时候也要注意,不能够直接调用。例如下面的方式就是错的

 console.log(obj1[Symbol("第一次")]);   //undefined

这个时候会返回结果undefined , 因为Symbol每次返回的都是一个不同的值,所以在调用Symbol的属性的时候需要先通过之前的Object.getOwnPropertySymbols(obj1)获取到symbol的属性名集合,然后再去通过这个属性名去调用

let sarr = Object.getOwnPropertySymbols(obj1);
console.log(sarr);   //Symbol属性名的数组
 
console.log(obj1[sarr[0]]);  //调取对象里面的Symbol属性

在之前学习的数据类型里面,Number里面等基本数据类型里面都有一些固定的值,例如Number.MAX_VALUENumber.MIN_VALUE。同样Symbol这个类型,也有几个特殊的固定值,最重要的一个就是Symbol.iterator

具体的使用请参看迭代器章节

生成器函数Generator

在之前的ES5里面,只要我们定义好方法以后,都是调用执行,调用以后都是立即执行(除延时调用与循环调用),但是生成器函数不一样

function abc(){
     console.log("hello");

     return "world";
 }
var str = abc();   //当我们调用abc()这个方法的时候,这个方法立即执行,并且返回了一个值给str
console.log(str);

下面请看ES6里面的生成器函数

function * abc(){
    console.log("hello");
    
    return "world";
}

生成器函数与普通函数不同,它在function 关键字与方法名之间加了一个*

生成器函数调用执行以后,这个方法没有立即执行,而是生成了一个方法执行状态,我们可以接收这个状态是暂停的,这个方法内部的代码还没有立即执行

let result=abc();    //这个result就是这个方法的执行状态

在这里,我们可以调用result.next()的方法让它继续执行代码

function * abc(){
    // console.log("方法开始");

    //和return 一模一样
    //当生成器的代码碰到了 yield以后,就会返回这个值,但是方法没有停止,,而是暂停
    //你下次再调用next()的方法的时候,代码就从这个暂停的地方开始
    yield "我";
    yield "爱";
    yield "北";
    yield "京";
    yield "的";
    yield "雪";
}

var result=abc();
// while((item=result.next()).done==false){
//     console.log(item.value)
// }

for(let item of result){
    console.log(item);
}

当生成器里面的函数碰到了yield以后,它就会暂停,同时,会把这个值先返回给你,如果你再次调用next()方法,则这个执行result会从上次暂停的地方继续执行,直到碰到下一个yield

yield关键字只能在生成器里面使用

迭代器(Iterable、Iterator)

Iterable:能力,可迭代的能力

Iterator:迭代器,它是一个接口

迭代器它是一个接口(当一个对象只要是实现了这个接口(Iterator),就具备接口的能力(Iterable)),迭代器它是一个能力,一个对象的内部如果有了迭代器这个东西,那么它就具备迭代的能力

迭代的能力就是可以从对象的内部一个一个取值的能力( 底层使用的还是生成器函数),在JS里面,有以下几种数据类型是自带迭代能力

Array,Set,Map,NodeList,arguments,TypedArray,String只要是具备迭代能力的,就一定可以遍历,也可以使用展开运算符

迭代器是一个一个的取值,它通过next()来获取结果,这个结果是一个对象,里在有一个value属性与一个done属性,当这个done的属性为true的时候,则代表没有取到值了,当一个元素要进行迭代器的遍历的时候,我们应该使用如下的方法来进行遍历

let s=new Set();

s.add("a");
s.add("b");
s.add("c");
s.add(123).add(456);
let result = s.values();   //它返回的类型是一个Iterable
let item=null;  //item用于存储得到的值
while((item=result.next()).done==false){
    console.log(item.value);
}

所有具备迭代器接口的集合都在ES6里面提供了一种新的遍历方法for...of,这一种遍历方法针对的不是索引,所以这一种遍历方法拿不到索引

let arr=["a","b","c","d"];
for(var item of arr){
    //item代表当前遍历的每一项
    console.log(item);

}

Set使用 for ... of遍历

let s=new Set();
s.add("a");
s.add("b");
s.add("c");
s.add(123).add(456);
for(var item of s){
    console.log(item);
}

自定义对象实现Iterable

之前在学习生成器函数的时候,我们已经知道 ,生成的yield就是一次一次的返回值,并且调用next()的方法继续执行,也可以使用for...of遍历,如果一个对象要实现Iterable迭代能力,怎么办呢

Symbol 的数据类型里面,有一个值叫 Symbol.iterator, 这个值就是就是迭代专有属性,只有一个对象有了这个属必到后就可以进行迭代了for...of

let obj = {
    userName: "杨标",
    sex: "男",
    age: 18,
    // 迭代是一个一个的取值
    [Symbol.iterator]: function* () {
        let i = 0;
        let keys = Object.keys(this);
        while (keys[i] !== undefined) {
            yield this[keys[i]];
            i++;
        }
    }
}
/*
for(let item of obj){
    console.log(item);
}
*/
console.log(...obj);

在上面的代码里面,我们在obj 对象里面实现了一个接口 [Symbol.iterator],实现这个接口以后,当前对象obj就具备了迭代 Iterable 的能力,如果我们希望把所有的对象都具备迭代能力,怎么处理

Object.prototype[Symbol.iterator]=function*(){
    let i = 0;
    let keys = Object.keys(this);
    while (keys[i] !== undefined) {
        yield this[keys[i]];
        i++;
    }
}

反射Reflect

反射是JS里面操作对象的另一种方式,它使用Reflect来进行,具体请看下面代码

let obj={
   userName:"标哥"
}
  1. defineProperty(target,property,descriptor)方法,定义某一个属性

    target代表要操作的目标对象,property要操作的属性,descriptor属性的描述信息

    //相当于在obj的对象上面定义了一个属性age
    Reflect.defineProperty(obj,"age",{
        value:18,
        writable:true,
        configurable:true,
        enumerable:true
        //这四个特性是之前我们es5里面所讲的4个特性
    });
    console.log(obj);
    
  2. deleteProperty(target,property)方法,删除某一个属性

    Reflect.deleteProperty(obj,"userName");
    console.log(obj);
    
  3. get(target,propertyKey,receiver),获取某一个对象的某一个属性值

    target代表要操作的对象,properyKey代表要操作的属性名,receiver如果是一个访问器属性,这个参数代表访问器属性里面的this

    let obj={
        userName:"杨标",
    	//访问器属性
        get myName(){
            return this.userName;
        }
    }
    let obj1={
        userName:"张三丰"
    }
    var str = Reflect.get(obj,"userName");
    console.log(str);   //杨标
    
    var str1=Reflect.get(obj,"myName");
    console.log(str1);  //杨标
    
    //receiver 第三个参数代表访问器属性里面的this
    var str2=Reflect.get(obj,"myName",obj1);
    console.log(str2);  //张三丰
    
  4. set(target,properyKey,value,receiver)设置某一个属性值

    参数说明请参考get的情况

    let obj={
        userName:"杨标",
        //它是一个特殊的属性,叫访问器属性
        get myName(){
            return this.userName;
        },
        set myName(newValue){
            this.userName=newValue;
        }
    }
    
    let obj1={
        userName:"张三丰"
    }
    //这个地方操作的是obj
    Reflect.set(obj,"userName","张无忌");
    console.log(obj);
    
    //这个地方操作的是obj1
    Reflect.set(obj,"userName","张无忌",obj1);
    console.log(obj1);
    
  5. has()判断当前对象是否有这个属性,无论是当前的还是父级的都可以,不可遍历(enumerable:false)属性也可以

    class Person{
        constructor(){
            this.userName="杨标"
        }
    }
    
    class Student extends Person{
        constructor(){
            super();
            this.sid="H19040001"
        }
    }
    
    let s=new Student();
    Reflect.defineProperty(s,"age",{
        value:18,
        enumerable:false,
        configurable:false,
        writable:false
    })
    
    //获取当前属性
    let flag1 = Reflect.has(s,"sid");
    console.log(flag1);   //true
    
    //获取父级属性
    let flag2=Reflect.has(s,"userName");
    console.log(flag2);  //true
    
    //获取不可遍历属性
    let flag3 = Reflect.has(s,"age");
    console.log(flag3);   //true 
    
  6. getProrotypeOf()获取当前构造方法的prototype

    class Person{
        constructor(){
            this.userName="杨标"
        }
    }
    class Student extends Person{
        constructor(){
            super();
            this.sid="H19040001"
        }
    }
    let p = Reflect.getPrototypeOf(Student);
    console.log(p);  //结果为Person的构造方法
    
  7. setPrototypeOf()给某一个对象设置一个父级对象(原型对象)

    class Person{
       constructor(){
           this.userName="杨标"
       }
    }
    class Student{
       constructor(){
           this.sid="H19040001"
       }
    }
    let p=new Person();
    let s=new Student();
    //给S指定一个父级p
    Reflect.setPrototypeOf(s,p);
    console.log(s.userName);  //杨标
    
  8. apply(target:function,thisArg,arguments)方法

    let obj={
        userName:"张三",
        sayHello(){
            console.log(this.userName);
        },
         getUserName(){
            return this.userName;
        }
    }
    let stu={
        userName:"李四"
    }
    obj.sayHello.apply(stu,[]);
    Reflect.apply(obj.sayHello,stu,[]);
    //当这个方法有返回值的时候,Reflect也有返回值
    let str = Reflect.apply(obj.getUserName,obj,[]);
    console.log(str);
    

    apply方法会改变方法内部的this指向,所以在使用Reflect.apply的时候,我们也可以改变里面this的指向

  9. constructor()执行某一个构造方法

    function Student(){
        console.log("我执行了");
        this.name="张三";
    }
    
    class Person{
        constructor(){
            this.age=18;
        }
    }
    
    let obj = Reflect.construct(Student,[]);
    console.log(obj);
    
    let obj2=Reflect.construct(Person,[]);
    console.log(obj2);
    

    constructor把方法当成构造方法去执行,返回创建的对象,[] 里面放的是参数

  10. ownKeys(),该方法与ES5里面的Object.getOwnPropertyNames()相同

    这个方法可以拿到enumerable:false的属性,但是Object.keys() 这个方法是获取不到的

    let obj={
        userName:"杨标",
        sex:"男"
    }
    
    Object.defineProperty(obj,"age",{
        value:18,
        enumerable:false
    })
    
    //Object.getOwnPropertyNames()可以获取刚才enumerable:false的
    
    let arr1=Object.keys(obj);
    console.log(arr1);   //[ 'userName', 'sex' ]
    
    let arr = Object.getOwnPropertyNames(obj);
    console.log(arr);   //[ 'userName', 'sex', 'age' ]
    
    let arr2 =  Reflect.ownKeys(obj);
    console.log(arr2);  //[ 'userName', 'sex', 'age' ]
    

? 思考

Reflect.has(),Object.hasOwnProperty(),Reflect.ownKeys()Object.getOwnPropertyNames(),Object.keys(),in它们对属性的检测分别是什么结果,请从不可遍历属性enumerable:false, 子级与父级的属性这些情况去判断

? 对比结果

  • Object.keys()只能获取自己的,并且是enumerable:true
  • for... in是检测这个属性在不在这个对象,同时也会去父级检测 ,但是不能检测enumerable:false

代理Proxy

将之前的一个对像的操作进行代理下来,不直接操作原来的对象,转进行操作代理对象,可以理解为把之前的ES5里面的对象的getset做了一次操作

let obj={
    userName:"张成",
    answer(){
         console.log("我没杀人")
     }
 };  //这是原始对象


 // 律师
 let lawyer=new Proxy(obj,{
    //  代理配置信息
    //get对属性取值
    get(target,properyName,receiver){
        // 在取值之前,可以进行一系列的操作
        //常规的代理是需要极反射结合在一起的
        if(properyName=="userName"){
            return "在我律师来之前,我保持沉默";
        }
        return Reflect.get(target,properyName,receiver);
    },
    //set对属性赋值
    set(target,properyName,value,receiver){
        //在Set的赋值之前,我们可以进行一系列的操作
        Reflect.set(target,properyName,value,receiver);
    }
 });

 console.log(lawyer.userName);

传统的ES5里面,只有get与set的操作能够被拦截,但是在ES6的代理里面,我们可以有很多操作都能实现

1569481566588

模块化

在很久很久以前,JS也在模块化,它有两个规范(AMD/CMD规范),后面慢慢被另一种方式取代了,叫CommonJS规范,而在ES6里面又出了一个模块化的规范叫ESModule(这个规范在Nodejs不支持,需要借用第三方平台 如webpack或glup等)

NodeJS是没有DOM的,所以它就不会有<script> 标签,没有这个标签JS文件之间怎么引用的呢?

在NodeJS里面提供一种特殊的文件引入机制(CommonJS)以实现模块化,它使用关键字module.exportsexports来完成

ES6本身是支持模块化的(ES Module),但是在NodeJS里面不支持

模块导入

CommonJS使用 require()有一份去导入一份JS文件时候,相当于把这个JS文件拿到了当前的JS文件里面去

Person.js文件

console.log("这是Person.js文件");

Student.js文件

require("./Person.js");
//相当于把Person.js文件拿到了Student.js 里面来

当我们去执行Student.js里面的代码的时候,Person.js

使用require() 方法去导入的时候,它会形成一个缓存,如果多次导入,它不会再去执行,所以当我们把Student.js的代码换成如下的时候

require("./Person.js");
require("./Person.js");
require("./Person.js");
require("./Person.js");
require("./Person.js");

我们去执行Student.js的时候,仍然只发现它控制台输出了一句话。这是因为第一次require()的时候它是真正的导入 ,后面要是发现了导入同样的文件,它会直接从缓存里面去拿值。

如果不希望它从缓存里面去拿,每次都从新导入 ,这个时候 ,我们需要在被导入的文件后面添加一句代码,删除缓存

console.log("这是Person.js文件");
//添加这样的代码,则表示 每次导入这个文件完成以后,就从缓存里面删除
delete require.cache[module.filename];

当我们在Person.js的文件里面添加了上面的代码以后,表示每次导入完成以后都从缓存里面删除自己这个模块。这个时候,当我们在Student.js里面多次导入Person.js的时候,我们会看到控制台输出了多条记录


当使用require()去导入 JS文件的时候,里面的代码会执行,但是在这个文件里面的变量,方法,对象等这些都不会拿过来。如果想拿到这些变量,方法与对象,则需要被导入的文件(这里指Person.js)手动导出来

模块导出

模块的导出使用的不是方法,而是两个对象

在NodeJS的模块化规范CommonJS里面,每个文件内部都有两个自带的对象,分别是exportsmodule.exports

exports它是一个指针,指向了module.exports,module.exports默认是一个空的对象,这个空对象就是CommonJS专门用于公开这个文件里面的变量,方法以及对象的集合

现在,我们针对导入与导出的情况做如下代码测试

person.js

console.log("这是Person.js文件");

let a="hello world";

module.exports=a;

Student.js

let a = require("./Person.js");

console.log(a);

通过上面的代码,我们已经可以看到a这个变量拿到了Stduent.js里面来执行,它没有报错,这就说明a被导出来了

如果我们需要导出多个变量或方法以及对象的时候,可以用如下的方式来进行


person.js

console.log("这是Person.js文件");

let a="hello world";

let b="你好,世界";

module.exports={
    a,b  //解构
}

student.js

// let obj = require("./Person.js");
// console.log(obj);

//导入以后直接解构
let {a,b} = require("./Person.js");

console.log(a);

console.log(b);

导出多个的时候,我们可以在导出的时候封装成一个对象,在导入的过程当中,我们也可以直接使用对象

exports与module.exports的区别

在之前,我们已经提过了,exports是一个指针,指向了module.exports,而CommonJS里面真正负责公开的是module.exports ,这两个东西最初是相等的,都是指内存推里面存放的一个空对象{}

如果我们在导出的时候,直接使用下面方式

let a="hello world";
exports=a;

这个时候是不行的,因为exports的内存栈地址已经不指向了module.exports,所以这个a是导不出去的

a.js代码

console.log("hello world");

let stra="变量stra";
exports=stra;
//exports已经不指向 module.exports
//真正的导出是module.exports   {}
//不允许直接更改exports的值,因为它是一个指针,指向module.exports

b.js代码

let stra = require("./a.js");

//这个地方的stra它是一个空对象
console.log(stra);

这个时候,得到的stra它仍然是一个空的对象{}

1569486771915

当我们对exports=stra赋值以后的内存图如下

1569486885562

如果真的要使用exports导出,则应该使用对象操作属性的方法,如下所示

c.js代码

let str="hello world";

exports.str=str;

exports.sayHello=function(){
    console.log("你好啊,世界")
}

d.js代码

let obj = require("./c.js");
console.log(obj);
//这个对象也可以直接解构

module.exports与exports最好不要同时使用,如果同时使用会出现exports找不到module.exports的情况

let str="hello world";
module.exports={
    abc:"abc"
}
exports.str=str;

exports.sayHello=function(){
    console.log("你好啊,世界")
}

如果是像上面这种方式去给的,那么exports 又会失效,这个时候的module.exports重新赋值的,而exports还指向原来的对象

1569487214101

根据内存图的解释 ,如果非要一起使用,我们都不应该改变内存地址

let str="hello world";
// module.exports={
//     abc:"abc"
// }

module.exports.abc="abc";   //不改变exports的地址,直接在这个对象上面的属性赋值
exports.str=str;
exports.sayHello=function(){
   console.log("你好啊,世界")
}


现在我们来实现一个完整的对象继承

Person.js代码

class Person{
    constructor(userName,sex,age){
        this.userName=userName;
        this.sex=sex;
        this.age=age;
    }
    sayHello(){
        console.log(`大家好,我叫${this.userName}, 我的性别是${this.sex},我的年龄是${age}`);
    }
}
module.exports=Person;

Student.js代码

let Person= require("./Person.js");
//这个Student如果想去继承Person对象,怎么办呢
class Student extends Person {
   constructor(userName,sex,age,sid){
       super(userName,sex,age);
       this.sid=sid;
   }
}
let s=new Student("杨标","男",18,"H19040001");
console.log(s.userName);
6

评论区