前端架构师之02_ES6_高级

1 类和继承

1.1 class类

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。

javascript 复制代码
// ES5 创建对象
// 创建一个类,用户名 密码
function User(name,pass){
    // 添加属性
    this.name = name;
    this.pass = pass;
}
// 用 原型 添加方法
User.prototype.showName=function(){
    // 输出名字
    alert(this.name);
}
User.prototype.showPass=function(){
    alert(this.pass);
}
// new 出来一个对象,用户名是admin,密码是123
var u1 = new User('admin','123');
u1.showName();
u1.showPass();

上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

javascript 复制代码
// ES6 创建对象
class User {
    // constructor 构造函数
    // 构造器:等价于es5中的构造函数
    constructor (name,pass){
        this.name = name;
        this.pass = pass;
    }
    // 注意,定义方法的时候,前面不需要加上function这个关键字
    // 直接把函数定义放进去了就可以了。
    // 另外,方法与方法之间不需要逗号分隔,加了会报错。
    showName() {
        alert('my name is ' + this.name);
    }
    showPass() {
        alert('my password is ' + this.pass);
    }
}

var u1 = new User('admin','123');
u1.showName();
u1.showPass();

ES6 的类,完全可以看作构造函数的另一种写法。

javascript 复制代码
// 类的数据类型就是函数,类本身就指向构造函数
class User {
    // ...
}

typeof User // "function"
User === User.prototype.constructor // true

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

构造函数的prototype属性,在 ES6 的"类"上面继续存在。

事实上,类的所有方法都定义在类的prototype属性上面。

constructor 方法

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。

一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。

javascript 复制代码
class User {
}

// 等同于
class User {
    constructor() {}
}

注:实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

1.2 static静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。

如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为"静态方法"。

javascript 复制代码
class Box{
    static a(){
        return "我是Box类中的,实例方法,无须实例化,可直接调用!"
    }
}
// 通过类名直接调用
// 我是Box类中的,实例方法,无须实例化,可直接调用!
console.log(Box.a());

注意:静态方法只能在静态方法中调用,不能在实例方法中调用

javascript 复制代码
class Box {
    static a() {
        return "我只允许被静态方法调用哦!"
    }
    static b() {
        // 通过静态方法b来调用静态方法a
        console.log(this.a());
    }
}
// 输出:我只允许被静态方法调用哦
Box.b();

1.3 继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

javascript 复制代码
// ES5继承
// 创造一个VIP用户,继承User普通用户
function User(name,pass){
    this.name=name;
    this.pass=pass;
}
User.prototype.showName=function(){
    alert(this.name);
}
User.prototype.showPass=function(){
    alert(this.pass);
}

// 继承用户
function VipUser(name,pass,level){
    User.call(this,name,pass);
    this.level=level; // VipUser 的属性
}
// 接收User里面的原型对象
VipUser.prototype=new User();
// 创建VipUser自己的原型对象
VipUser.prototype.showLevel=function(){
    alert(this.level);
}
// 传入参数,用户名,密码,等级
var v1=new VipUser('zs','111','3');
v1.showName();
v1.showPass();
v1.showLevel();
javascript 复制代码
// ES6中的继承
class User {
    constructor (name,pass){
        this.name=name;
        this.pass=pass;
    }
    
    showName(){
        return this.name;
    }
    
    showPass(){
        alert(this.pass);
    }
}

// extends 继承,扩展
class VipUser extends User{
    // constructor 创建属性 子类也有自己的类,也有自己的属性(继承的属性在前,新创建的在后)
    constructor(name,pass,level){
        // 调用父类的 constructor(x, y)
        super(name,pass);
        // 自己的属性
        this.level=level;
    }
    // 方法在这里不需要继承原来的方法了,因为super已经把父级的方法都继承过来了
    // 直接添加新东西就行了
    showName(){
        return this.name + ' ' + super.showName();
    }
    showLevel(){
        alert(this.level);
        super.show
    }
}
// 直接调用就可以了
var v1=new VipUser('zs','111','3');

v1.showName();
v1.showPass();
v1.showLevel();

super在这里表示父类的构造函数,用来新建一个父类的实例对象。

子类必须在constructor()方法中调用super(),否则就会报错。

为什么子类的构造函数,一定要调用super()?

  • 原因就在于 ES6 的继承机制,与 ES5 完全不同。
  • ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即"实例在前,继承在后"。
  • ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即"继承在前,实例在后"。
  • 这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。
  • 注意,这意味着新建子类实例时,父类的构造函数必定会先运行一次。
  • 另外,在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。
  • 这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。

2 Promise对象

Promise是异步编程的一种解决方案,比传统的解决方案------回调函数和事件------更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。

在使用ES5的时候,在多层嵌套回调时,写完的代码层次过多,很难进行维护和二次开发,ES6认识到了这点问题,现在promise的使用,完美解决了这个问题。

2.1 回调地狱

现在有6个div,我想给每个div都添加一个移动的动画,并且先执行第一个,再执行第二个,再执行第三个,以此类推。

javascript 复制代码
// 回调地狱 : 回调函数多层嵌套
$(".scene p").eq(0).animate({
    marginTop: '-200px'
}, 1000, function () {
    $(".scene p").eq(1).animate({
        marginTop: '200px'
    }, 1000, function () {
        $(".scene p").eq(2).animate({
            width: '200px'
        }, 1000, function () {
            $(".scene p").eq(3).animate({
                height: '200px'
            }, 1000, function () {
                $(".scene p").eq(4).animate({
                    marginLeft: '-200px'
                }, 1000, function () {
                    $(".scene p").eq(5).animate({
                        marginTop: '200px'
                    },1000)
                })
            })
        })
    })
})

虽然可以实现效果,但是需要嵌套很多层的回调函数,如果需求量增多,回调层级会更多,我们把这种多次的回调称之为 "回调地狱"。

promise用来解决回调地狱的问题,把异步的代码用同步的方式来实现。

2.2 基本使用

Promise对象是一个构造函数,用来生成Promise实例。

javascript 复制代码
const promise = new Promise(function(resolve, reject) {
    // ... some code
    
    if (/* 异步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

Promise执行多步操作非常好用,那我们就来模仿一个多步操作的过程。

比如,早上起床上课分了几个步骤:

  1. 起床洗漱
  2. 食堂吃饭
  3. 教室上课
javascript 复制代码
let state = 1;
function step1(resolve, reject) {
    console.log('起床洗漱');
    if (state == 1) {
        resolve('洗漱完成');
    } else {
        reject('洗漱失败');
    }
}

function step2(resolve, reject) {
    console.log('食堂吃饭');
    if (state == 1) {
        resolve('吃饭完成');
    } else {
        reject('吃饭失败');
    }
}

function step3(resolve, reject) {
    console.log('教室上课');
    if (state == 1) {
        resolve('上课完成');
    } else {
        reject('上课失败');
    }
}

new Promise(step1).then(function (val) {
    console.log(val);
    return new Promise(step2);
}).then(function (val) {
    console.log(val);
    return new Promise(step3);
}).then(function (val) {
    console.log(val);
    return val;
});

2.3 promise方法

promise的all方法和race方法

  • all:当两个异步操作都成功完成后,再执行的逻辑
  • race:比赛;谁快获取谁;最先得到的异步操作,即执行下面的业务逻辑
  • all()和race()中的参数必须是promise实例
javascript 复制代码
// all和race的区别
// all:100个人跑步跑步:等100个跑到终点才结束
// race:只要第一个跑到终点就结束,后面的99个就不管了
Promise.all([new Promise(step1), new Promise(step2)]).then(function (res) {
    console.log(res);
});

Promise.race([new Promise(step1), new Promise(step2)]).then(function (res) {
    console.log(res);
});

3 Proxy预处理

当我们在操作一个对象或者方法时会有几种动作,比如:在运行函数前初始化一些数据,在改变对象值后做一些善后处理。这些都是预处理函数,也叫做钩子函数。

Proxy的存在就可以让我们给函数加上这样的钩子函数,你也可以理解为在执行方法前预处理一些代码。你可以简单的理解为他是函数或者对象的生命周期。

声明Proxy

我们用new的方法对Proxy进行声明。可以看一下声明Proxy的基本形式。

javascript 复制代码
new Proxy({},{});

这里有两个花括号,第一个花括号就相当于我们方法的主体,后边的花括号就是Proxy代理处理区域,相当于我们写钩子函数的地方。

实现一个Proxy函数

javascript 复制代码
var pro = new Proxy({
    add: function (val) {
        return val + 10;
    },
    name: 'Hello World!'
}, {
    get:function(target,key,property){
        console.log('调用Get方法');
        return target[key];
    }
});

console.log(pro.name);
// 先输出了come in Get。相当于在方法调用前的钩子函数。

get属性

get属性是在你得到某对象属性值时预处理的方法,他接受三个参数

  • target:目标对象
  • key:属性名
  • property:proxy 实例本身

set属性

set属性是值你要改变Proxy属性值时,进行的预先处理。它接收四个参数

  • target:目标对象
  • key:属性名
  • value:属性值
  • receiver:Proxy 实例本身
javascript 复制代码
var pro = new Proxy({
    add: function (val) {
        return val + 10;
    },
    name: 'Hello World!'
}, {
    get: function (target, key) {
        console.log('调用Get方法');
        return target[key];
    },
    set: function (target, key, value, receiver) {
        console.log(`调用Set方法,设置值 ${key} = ${value}`);
        return target[key] = value;
    }
});

console.log(pro.name);
pro.name = '你好世界!';
console.log(pro.name);

4 初识模块化开发

模块化是软件的一种开发方式,利用模块化可以把一个非常复杂的系统结构细化到具体的功能点,每个功能点看作一个模块,然后通过某种规则把这些小的模块组合到一起,构成模块化系统。

我们从一开始学习前端,我们没有使用模块化,主要在我们对应的js这里的代码。我们就是把相关功能放在我们对应的js中,在页面中引入js来完成相关的功能。

4.1 传统JavaScript开发的弊端

传统浏览器端JavaScript在使用的时候存在的两大问题

  • 文件依赖
    • 在JavaScript中文件的依赖关系是由文件的引入先后顺序决定的。在开发过程中,一个页面可能需要多个文件依赖,但是仅从代码上是看不出来各个文件之间的依赖关系,这种依赖关系存在不确定性。如果更改文件的引入先后顺序,就很有可能导致程序错误。
  • 命名冲突
    • 在JavaScript中,文件与文件之间是完全开放的,并且语法本身不严谨,如果在后续引入的文件中声明了一个同名变量,则后面文件的变量会覆盖前面文件中的同名变量,这样会导致程序存在潜在的不确定性。

4.2 模块化的概念

**现实生活中手机的模块化 **

从生产角度来看,模块化是一种生产方式,体现了以下两个特点:

  • 生产效率高:灵活架构,焦点分离;多人协作互不干扰;方便模块间组合、分解。
  • 维护成本低:可分单元测试;方便单个模块功能调试、升级。

软件中的模块化开发

从程序开发角度,模块化是一种开发模式,有以下两个特点:

  • 生产效率高:方便代码重用,别人开发好的模块功能可以直接拿过来使用,不需要重复开发类似的功能。
  • 维护成本低:软件开发周期中,由于需求经常发生变化,最长的阶段并不是开发阶段,而是维护阶段,使用模块化开发的方式更容易维护。

5 模块成员的导入和导出

5.1 exports和require()

在模块化开发中,一个JavaScript文件就是一个模块,模块内部定义的变量和函数默认情况下在外部无法得到。

如何得到模块内部定义的变量和函数呢?

Node.js为开发者提供了一个简单的模块系统,exports是模块公开的接口,require()用于从外部获取一个模块的接口,即获取模块的exports对象。

如何在一个文件模块中获取其他文件模块的内容?

  • 首先需要使用require()方法加载模块;
  • 然后在被加载的模块中使用exports或者module.exports对象向外开放变量、函数等;require()函数的作用是加载文件并获取该文件中的module.exports对象接口。
javascript 复制代码
// 新建info.js文件作为被加载模块
// 声明一个add()函数用来实现加法功能
const add = (n1, n2) => n1 + n2;
// exports对象向模块外开放add()函数
exports.add = add;

// 新建b.js文件,实现在b.js模块中导入info.js模块
// 模块导入时,模块的后缀.js可以省略
const info = require('./info');
// 结果为:30
console.log(info.add(10, 20));

// 打开命令行工具,切换到b.js文件所在的目录,并输入"node b.js"命令。

总结Node.js的模块化开发的步骤:

  • 通过exports对象对模块内部的成员进行导出。
  • 通过require()方法对依赖的模块进行导入操作。

5.2 module.exports

javascript 复制代码
// 新建info.js文件作为被加载模块
// 声明一个greeting()函数用于实现打招呼功能
const greeting = name => `hello ${name}`;
// 使用module.exports对象向模块外开放greeting()函数
module.exports.greeting = greeting;

// 新建a.js文件,实现在a.js模块中导入info.js模块
// 模块导入时,模块的后缀.js可以省略
const a = require('./info');
// 输出模块中函数的值:hello zhangsan
console.log(a.greeting('zhangsan'));

// 打开命令行工具,切换到a.js文件所在的目录,并输入"node a.js"命令。

5.3 exports和module.exports的区别

exports和module.exports都可以对外开放变量或函数,那么它们之间有什么区别?

exports和module.exports的区别

  • Node.js提供的exports对象是module.exports对象的别名(地址引用关系),导出对象最终以module.exports对象为准。
  • 在使用上,module.exports对象可以单独定义返回数据类型,而exports对象只能是返回一个object对象。
  • 默认情况下,exports和module.exports指向同一个对象,也就是说指向同一个内存空间;
  • 当exports和module.exports指向两个不同对象时,导出对象最终以module.exports对象的导出为准。

exports和module.exports指向同一个对象的情况

javascript 复制代码
// 新建info.js文件作为被加载模块
const greeting = name => `hello ${name}`;
const x = 100;
// 使用exports对象导出x变量
exports.x = x;
// 使用module.exports对象导出greeting()函数
module.exports.greeting = greeting;

// 新建a.js文件,实现在a.js模块中导入info.js模块
// 模块导入时,模块的后缀.js可以省略
const a = require('./info');
// 输出模块中函数的值
console.log(a);

// 打开命令行工具,切换到a.js文件所在的目录,并输入"node a.js"命令。

当exports和module.exports指向同一个对象时,以下两种写法是等价的:

  • exports.属性名 = 属性值;
  • module.exports.属性名 = 属性值;

exports和module.exports指向不同对象时的情况

javascript 复制代码
// 新建info.js文件作为被加载模块
const greeting = name => `hello ${name}`;
const x = 100;
exports.x = x;
module.exports.greeting = greeting;
// 使用module.exports重新指向一个属性名为name,值为zhangsan的对象
module.exports = {
    name: 'zhangsan',
};

// 新建a.js文件,实现在a.js模块中导入info.js模块
// 模块导入时,模块的后缀.js可以省略
const a = require('./info');
// 输出模块中函数的值
console.log(a);

// 打开命令行工具,切换到a.js文件所在的目录,并输入"node a.js"命令。

当exports和module.exports指向不同对象时,以module.exports对象的导出结果为准。

6 模块化操作

在ES5中我们要进行模块华操作需要引入第三方类库,随着前后端分离,前端的业务日渐复杂,ES6为我们增加了模块化操作。

现在前端开发的主角,是基于ESM(ES6 Module),利用ESM操作,可以让我们更方便的进行模块化开发。

模块化操作主要包括两个方面。

  • export:负责进行模块化,也是模块的输出。
  • import:负责把模块引,也是模块的引入操作。

想要使用ES6语法需要给script标签添加上 type="module" 属性,但是这个方式只适合测试使用,因为兼容性比较差,建议项目中使用插件解决对应的转化,可以使用nodejs解决。

基本用法

export可以让我们把变量,函数,对象进行模块话,提供外部调用接口,让外部进行引用。

注:关键词 export {} 后面的花括号不能少。

javascript 复制代码
//temp.js
export var a = 'Hello World!';

// 然后在index.js中以import的形式引入。
// index.js
import { a } from './temp.js';
console.log(a);

注:引入进来的内容必须用{}包括起来,路径中必须用必须添加./或者.../或者/ 否则会报错。

这就是一个最简单的模块的输出和引入。

多变量输出

声明3个变量,需要把这3个变量都进行模块化输出,这时候我们给他们包装成对象就可以了。

javascript 复制代码
var a = '你好世界';
var b = 'HelloWorld';
var c = 'H5website';

export {a,b,c}

函数的模块化输出

javascript 复制代码
export function add(a,b){
    return a + b;
}

as的用法

有些时候我们并不想暴露模块里边的变量名称,而给模块起一个更语义话的名称,这时候我们就可以使用as来操作。

javascript 复制代码
var a = '你好世界';
var b = 'HelloWorld';
var c = 'H5website';

export {
    a as x,
    b as y,
    c as z
}

import * as abc from './地址';

导入时使用 星号 表示全部

default的使用

加上default相当是一个默认的入口。在一个文件里export default只能有一个。

默认入口导入时不需要再加大括号,因为只可能对应一个默认的。

我们来对比一下export和export default的区别。

javascript 复制代码
export var a ='HelloWorld';
export function add(a,b){
    return a+b;
}

// 对应导入方式
import { a,add } form './temp';//也可以分开写
javascript 复制代码
let a = 'HelloWorld';
export default a;

// 对应导入方式
import str from './temp';
相关推荐
10年前端老司机1 小时前
什么!纯前端也能识别图片中的文案、还支持100多个国家的语言
前端·javascript·vue.js
摸鱼仙人~1 小时前
React 性能优化实战指南:从理论到实践的完整攻略
前端·react.js·性能优化
程序员阿超的博客2 小时前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 2452 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
小小小小宇7 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖7 小时前
http的缓存问题
前端·javascript·http
小小小小宇7 小时前
请求竞态问题统一封装
前端
loriloy7 小时前
前端资源帖
前端
源码超级联盟7 小时前
display的block和inline-block有什么区别
前端
GISer_Jing8 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js