Bootstrap

JavaScript面向对象和ES6笔记

JavaScript面向对象

1.面向对象编程介绍

1.1 两大编程思想

  • 面向过程
  • 面向对象

1.2 面向过程编程POP(Process-oriented programming)

**面向过程**就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了

举个栗子:将大象装进冰箱,面向过程做法

  1. 打开冰箱门
  2. 大象装进去
  3. 关上冰箱门

面向过程,就是按照我们分析好了的步骤,按照步骤解决问题

1.3 面向对象编程OOP(Object Oriented Programming)

**面向对象**是把事务分解成为一个个对象,然后由对象之间分工与合作

举个栗子:将大象装进冰箱,面向对象做法:

先找出对象,并写出这些对象的功能:

  1. 大象对象
  • 进去
  1. 冰箱对象
  • 打开
  • 关闭
  1. 使用大象和冰箱的功能

面向对象是以对象功能来划分问题,而不是步骤。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工

面向对象编程具有灵活、代码可复用,容易维护和开发的优点,更适合多人合作的大型软件项目

面向对象的特性:

  • 封装性
  • 继承性
  • 多态性

1.4 面向对象和面向过程的对比

面向过程
  • 优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程
  • 缺点:没有面向对象易维护、易复用、易扩展
面向对象
  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
  • 缺点:性能比面向过程低

用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭

2.ES6中的类和对象

面向对象

面向对象更贴近我们的实际生活,可以使用面向对象描述现实事物,但是事物分为具体的事物和抽象的事物

  1. 抽取(抽象)对象共用的属性和行为组织(封装)成一个类(模板)
  2. 对类进行实例化,获取类的对象

面向对象编程我们考虑的是有哪些对象,按照面向对象的四维特点,不断的创建对象,使用对象,指挥对象做事情

2.1 对象

现实生活中:万物皆对象,对象是一个具体的事物,看得见摸的着的实物,例如,一本书、一辆汽车、一个人可以是"对象", 一个数据库、一张网页,一个与远程服务器的连接也可以是"对象"

在JavaScript中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象,例如字符串,数值,数组、函数等

对象是由属性方法组成的:

  • 属性:事物的**特征,在对象中用属性**来表示(常用名词)
  • 方法:事物的**行为,在对象中用方法**来表示(常用动词)

2.2 类 class

在ES6中新增加了类的概念,可以使用class关键字声明一个类,之后以这个类来实例化对象

抽象了对象的公共部分,它泛指某一大类(class)

对象特指某一个,通过类实例化一个具体的对象

面向对象的思维特点:

  1. 抽取(抽象)对象共用的属性和行为组织(封装)成一个****(模板)
  2. 对类进行实例化,获取类的**对象**

2.3 创建类

语法:
 class name {
     // class body
 }
创建实例:
 var xx = new name();

注意:类必须使用new实例化对象

2.4 类constructor构造函数

constructor()方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过new命令生成对象实例时,自动调用该方法。如果没有显示定义,类内部会自动给我们创建一个constructor()

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>创建类和对象</title>
</head>
<body>
    
</body>
</html>

<script>
    // 1. 创建类 class 创建一个明星类
    class Star {
        constructor(uname, age) {
            this.uname = uname;
            this.age = age;
        }
    }
    // 2. 利用类创建对象 new
    var ldh = new Star('刘德华', 18);
    var zxy = new Star('张学友', 20);
    console.log(ldh);
    console.log(zxy);
    // (1) 通过class 关键字创建类, 类名我们还是习惯性定义首字母大写
    // (2) 类里面有个constructor函数, 可以接受传递过来的参数, 同时返回实例对象
    // (3) constructor 函数 只要 new 生成实例时, 就会自动调用这个函数, 如果我们不写这个函数, 类也会自动生成这个函数
    // (4) 生成实例 new  不能省略
    // (5) 最后注意语法规范, 创建类 类名后面不要加小括号, 生成实例 类名后面加小括号, 构造函数不需要加function
</script>

2.5 类添加方法

语法:

 class Person {
	constructor(name, age) {
		this.name = name;
        this.age = age;
    }
     say() {
		console.log(this.name + '你好')
     }
 }
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>类中添加方法</title>
</head>
<body>
    
</body>
</html>

<script>
    // 1. 创建类 class 创建一个明星类
    class Star {
        // 类的共有属性放到 constructor 里面
        constructor(uname, age) {
            this.uname = uname;
            this.age = age;
        }
        sing(song) {
            // console.log(this.uname + '唱歌');
            console.log(this.uname + '唱' + song);
        }
    }
    // 2. 利用类创建对象 new
    var ldh = new Star('刘德华', 18);
    var zxy = new Star('张学友', 20);
    console.log(ldh);
    console.log(zxy);
    // (1) 我们类里面所有的函数不需要写function
    // (2) 多个函数或者方法之间不需要添加逗号分隔
    ldh.sing('冰雨')
    zxy.sing('李香兰')
</script>

3.类的继承

3.1 继承

现实中的继承:子承父业,比如我们都继承了父亲的姓

程序中的继承:子类可以继承父类的一些属性和方法

语法:
 class Father { // 父类
}
 class Son extends Father { // 子类继承父类
     
 }

3.2 super关键字

super关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>类的继承</title>
</head>
<body>
    
</body>
</html>

<script>
    // 1. 类的继承
    // class Father {
    //     constructor() {

    //     }
    //     money() {
    //         console.log(100);
    //     }
    // }
    // class Son extends Father {

    // }
    // var son = new Son()
    // son.money()
    class Father {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
        sum() {
            console.log(this.x + this.y);
        }
    }
    class Son extends Father {
        constructor(x, y) {
            super(x, y) // 调用了父类中的构造函数
        }
    }
    var son = new Son(1, 2);
    var son1 = new Son(11, 22);
    son.sum();
    son1.sum();
</script>
语法:
 class Father {
	say() {
		return '俺がお父さんだ'
    }	
 }

 class Son extends Father {
	say() {
		// super.say()  super 调用父类的方法
        return super.say() + 'の息子'
    }
 }

 var damao = new Son();
 console.log(damao.say())
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>super关键字调用父类普通函数</title>
</head>
<body>
    
</body>
</html>

<script>
    // super 关键字调用父类普通函数
    class Father {
        say() {
            return '俺がお父さん'
        }
    }
    class Son extends Father  {
        say() {
            console.log('娘だよ・えへっ');
            console.log(super.say() + 'の娘だ');
            // super.say() 就是调用父类中的普通函数 say()
        }
    }
    var son = new Son();
    son.say()
    // 继承中的属性或者方法查找原则: 就近原则
    // 1. 继承中, 如果实例化子类输出一个方法,先看子类有没有这个方法, 如果有就先执行子类的
    // 2. 继承中, 如果子类里面没有,就去查找父类有没有这个方法, 如果有, 就执行父类的这个方法(就近原则)
</script>
语法:
	class Person { // 父类
        consrtuctor(surname) {
            this.surname = surname;
        }
    }
	class Student extends Person {		// 子类继承父类
		constructor(surname, firstname) {
            super(surname);				// 调用父类的consturctor(surname)
            this.firstname = firstname; // 定义子类独有的属性
        }
    }
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>子类继承父类方法同时扩展自己方法</title>
</head>

<body>

</body>

</html>

<script>
    // 父类有加法方法
    class Father {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
        sum() {
            console.log(this.x + this.y);
        }
    }
    // 子类继承父类加法方法, 同时扩展减法方法
    class Son extends Father {
        constructor(x, y) {
            // 利用super 调用父类的构造函数
            // super 必须在子类this之前调用
            super(x, y)
            this.x = x;
            this.y = y;
        }
        subtract() {
            console.log(this.x - this.y);
        }
    }
    var son = new Son(5, 3)
    son.subtract();
</script>

注意:子类在构造函数中使用super,必须放到this前面(必须先调用父类的构造函数,再使用子类构造方法)

三个注意点:

  1. 在ES6中类没有变量提升,所以必须先定义类,才能通过类实例化对象
  2. 类里面的共有的属性和方法一定要加this使用。
  3. 类里面this指向问题

4. 面向对象案例

面向对象版tab栏切换

功能需求:

  1. 点击tab栏,可以切换效果
  2. 点击+号,可以添加tab项和内容项
  3. 点击×号,可以删除当前的tab项和内容项
  4. 双击tab项文字或者内容项文字可以修改里面的文字内容
案例分析

抽象对象:Tab对象

  1. 该对象具有切换功能
  2. 该对象具有添加功能
  3. 该对象具有删除功能
  4. 该对象具有修改功能
面向对象版tab栏切换 添加功能
  1. 点击 + 可以实现添加新的选项卡和内容
  2. 第一步:创建新的选项卡li和新的内容section
  3. 第二步:把创建的两个元素追加到对应的父元素中。
  4. 以前的做法:动态创建元素createElement,但是元素里面内容较多,需要innerHTML赋值再appendChild追加到父元素里面。
  5. 现在高级做法:利用insertAdjacentHTML()可以直接把字符串格式元素添加到父元素中。
面向对象版tab栏切换 删除功能
  1. 点击×可以删除当前的li选项卡和当前的section
  2. ×是没有索引号的,但是它的父元素li有索引号,这个索引号正是我们想要的索引号
  3. 所以核心思路是:点击×号可以删除这个索引号对应的li和section
面向对象版tab栏切换 删除功能
  1. 双击选项卡li或者section里面的文字,可以实现修改功能
  2. 双击事件是:ondblclick
  3. 如果双击文字,会默认选定文字,此时需要双击禁止选中文字
  4. window.getSelection?window.getSelection().removeAllRanges():document.selection.empty();
  5. 核心思路:双击文字的时候,在里面生成一个文本框,当失去焦点或者按下回车然后把文本框输入的值给原先元素即可。

构造函数和原型

1. 构造函数和原型

1.1 概述

在典型的OOP的语言中(如Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS中并没有引入类的概念。

ES6,全称ECMAScript6.0,2015.06发版。但是目前浏览器的JavaScript是ES5版本,大多数高版本的浏览器也支持ES6,不过只实现了ES6的部分特性和功能。

在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。

创建对象可以通过以下三种方式:

  1. 对象字面量
  2. new Object()
  3. 自定义构造函数

1.2 构造函数

构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。

在JS中,使用构造函数时要注意以下两点:

  1. 构造函数用于创建某一类对象,其首字母要大写
  2. 构造函数要和new一起使用才有意义
new在执行时会做四件事情
  1. 在内存中创建一个新的空对象
  2. 让this指向这个新的对象
  3. 执行构造函数里面的代码,给这个新对象添加属性和方法
  4. 返回这个新对象(所以构造函数里面不需要return)

JavaScript的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的this上添加。通过这两种方式添加的成员,就分别称为静态成员实例成员

  • 静态成员:在构造函数本身上添加的成员称为静态成员,只能由构造函数本身来访问
  • 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
 // 构造函数中的属性和方法我们称为成员,成员可以添加
    function Star(uname, age) {
        this.uname = unmae;
        this.age = age;
        this.sing = function() {
            console.log('我会唱歌');
        }
    }

    var ldh = new Star('刘德华', 18);
    // 实例成员就是构造函数内部通过this添加的成员 uname age sing 就是实例成员
    // 实例成员只能通过实例化的对象来访问
    console.log(ldh.age);
    console.log(Star.age);  // 不可以通过构造函数来访问实例成员
    // 2. 静态成员,在构造函数本身上添加的成员 sex 就是静态成员
    Star.sex = '男';
    console.log(Star.sex);
    console.log(ldh.sex);  // 不能通过对象来访问

1.3 构造函数的问题

构造函数方法很好用,但是存在浪费内存的问题。

 function Star(uname, age) {
        this.uname = unmae;
        this.age = age;
        this.sing = function() {
            console.log('我会唱歌');
        }
    }
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 21);
console.log(ldh.sing === zxy.sing)

1.4 构造函数原型prototype

构造函数通过原型分配的函数是所有对象所**共享的**

JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。

问答?

  1. 原型是什么?

​ 一个对象,我们也称为prototype为原型对象

  1. 原型的作用是什么?

共享方法

1.5 对象原型 _proto_

对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。

  • __proto__对象原型和原型对象prototype是等价的
  • __proto__对象原型的意义就在于为对象的查找机制提供了一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype
function Star(uname, age) {
        this.uname = uname;
        this.age = age;
    }

    Star.prototype.sing = function() {
        console.log(`${this.uname}会唱歌`);
    }

    var ldh = new Star('刘德华', 18);
    var zxy = new Star('张学友', 21);
    ldh.sing();
    console.log(ldh);  // 对象身上系统自己添加一个__proto__指向我们构造函数的原型对象prototype
    // 方法的查找规则:首先看ldh对象身上是否有sing方法,如果有就执行这个对象的sing; 
    // 如果没有sing这个方法,因为有__proto__的存在,就去构造函数原型对象prototype身上去查找sing这个方法

1.6 constructor 构造函数

对象原型(__proto__)构造函数(prototype)原型对象里面都有一个属性constructor属性,constructor我们称为构造函数,因为它指向构造函数本身。

constructor主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

1.7 构造函数、实例、原型对象三者之间的关系

Star.prototype
Star.prototype.constructor
ldh.__proto__
ldh.__proto__.constructor
Star构造函数
Star原型对象
prototype
ldh 对象实例

1.8 原型链

Star.prototype
Star.prototype.constructor
ldh.__proto__
ldh.__proto__.constructor
Star原型对象prototype.__proto__
Object原型对象.constructor
Object.prototype
Object原型对象prototype.__proto__
Star构造函数
Star原型对象
prototype
ldh 对象实例
Object原型对象
prototype
Object构造函数
null

1.9 JavaScript的成员查找机制(规则)

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是__proto__指向的prototype原型对象)。
  3. 如果还没有就查找原型对象的原型(Object的原型对象)。
  4. 依此类推一直找到Object为止(null)。
  5. __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

1.10 原型对象this指向

function Star(uname, age) {
        this.uname = uname;
        this.age = age;
    }
    var that;
    Star.prototype.sing = function() {
        console.log(`${this.uname}会唱歌`);
        that = this;
    }
    var ldh = new Star('刘德华', 18);
    // 1. 在构造函数中,里面this指向的是实例对象ldh
    ldh.sing();
    console.log(that === ldh);
    // 2. 原型对象函数里面的this指向的是实例对象ldh

1.11 扩展内置对象

可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能。

注意:数组和字符串内置对象不能给原型对象覆盖操作Array.prototype = {}, 只能是Array.prototype.xxx = function() {}的方式。

2. 继承

ES6之前并没有给我们提供extends继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承

2.1 call()

调用这个函数,并且修改函数运行时的this指向

fun.call(thisArg, arg1, arg2, ...)
  • thisArg:当前调用函数this的指向对象
  • arg1,arg2:传递的其他参数
function fn(x, y) {
        console.log('我想喝丝袜奶茶');
        console.log(this);
        console.log(x + y);
    }
    var o = {
        name: 'andy'
    }
    // fn();
    // 1. call() 可以调用函数
    fn.call();
    // 2. call() 可以改变这个函数的this指向
    fn.call(o, 1, 2);

2.2 借用构造函数继承父类属性

核心原理:通过call()把父类型的this指向子类型的this,这样就可以实现子类型继承父类型的属性。

 // 借用父构造函数继承属性
    // 1. 父构造函数
    function Father(uname, age) {
        // this 指向父构造函数的实例对象
        this.uname = uname;
        this.age = age;
    }

    // 2. 子构造函数
    function Son(uname, age, score) {
        // this 指向子构造函数的实例对象
        Father.call(this, uname, age);
        this.score = score;
    }

    var son = new Son('刘德华', 18, 100)
    console.log(son);

2.3 借用原型对象继承父类型方法

 // 1. 父构造函数
    function Father(uname, age) {
        // this 指向父构造函数的实例对象
        this.uname = uname;
        this.age = age;
    }
    Father.prototype.money = function() {
        console.log(1000000);
    }

    // 2. 子构造函数
    function Son(uname, age, score) {
        // this 指向子构造函数的实例对象
        Father.call(this, uname, age);
        this.score = score;
    }
    // Son.prototype = Father.prototype; 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化
    Son.prototype = new Father();
    // 如果利用对象的形式修改了原型对象,别忘了利用constructor指回原来的原型对象
    Son.prototype.constructor = Son;
    // 这个是子构造函数专门的方法
    Son.prototype.exam = function() {
        console.log('孩子要考试');
    }
    var son = new Son('刘德华', 18, 100);
    console.log(son);
    console.log(Father.prototype);
    console.log(Son.prototype.constructor);

3. 类的本质

  1. class本质还是function
  2. 类的所有方法都定义在类的prototype属性上
  3. 类创建的实例,里面也有__proto__指向类的prototype原型对象
  4. 所有ES6的类它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
  5. 所有ES6的类其实就是语法糖
  6. 语法糖:语法糖就是一种便捷写法。简单理解,有两种方法可以实现同样的功能,但是一种写法更加清晰、方便,那么这个方法就是语法糖

4. ES5中新增的方法

4.1 ES5新增方法概述

ES5中给我们新增了一些方法,可以很方便的操作数组或者字符串,这些方法主要包括:

  • 数组方法
  • 字符串方法
  • 对象方法

4.2 数组方法

迭代(遍历)方法:forEach()、map()、filter()、some()、every();

	array.forEach(function(currentValue, index, arr))
  • currentValue:数组当前项的值
  • index:数组当前项的索引
  • arr:数组对象本身
	// forEach迭代(遍历数组)
	var arr = [1, 2, 3];
    arr.forEach(function(value, index, array) {
        console.log(`每个数组元素${value}`);
        console.log(`每个数组元素的索引号${index}`);
        console.log(`数组本身${array}`);
    })
	array.filter(function(currentValue, index, arr))
  • filter()方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组
  • 注意它直接返回一个新数组
  • currentValue:数组当前项的值
  • index:数组当前项的索引
  • arr:数组对象本身
  // filter筛选数组
    var arr = [22, 44, 55, 66, 88, 12, 1, 3, 55, 9];
    var newArr = arr.filter(function(value, index) {
        // return value >= 20;
        return value % 2 !== 0;
    })
    console.log(newArr);
	array.some(function(currentValue, index, arr))
  • some()方法用于检测数组中的元素是否满足指定条件。通俗点查找数组中是否有满足条件的元素
  • 注意它返回值是布尔值,如果查找到这个元素,就返回true,如果查找不到就返回false
  • 如果找到第一个满足条件的元素,则终止循环,不在继续查找
  • currentValue:数组当前项的值
  • index:数组当前项的索引
  • arr:数组对象本身
 // some 查找数组中是否有满足条件的元素
    var arr = [10, 30, 4, 44];
    var flag = arr.some(function(value) {
        // return value >= 20;
        return value <= 2;
    })
    console.log(flag);

    var arr1 = ['red', 'blue', 'pink'];
    var flag1 = arr1.some(function(value) {
        return value == 'pink';
    })
    console.log(flag1);
    // 1. filter 也是查找满足条件的元素返回的是一个数组,而且是把所有满足条件的元素返回回来
    // 2. some 也是查找满足条件的元素返回的是布尔值,如果查找到第一个满足条件的元素就终止循环
查询商品案例
  1. 把数据渲染到页面中(forEach)
  2. 根据价格显示数据(filter)
  3. 根据商品名称显示数据
	var arr = ['red', 'green', 'blue', 'pink'];
    // 1. forEach迭代 遍历
    // arr.forEach(function(value) {
    //     if (value === 'green') {
    //         console.log('找到了该元素');
    //         return true;  // 在forEach里面return不会终止迭代
    //     }
    //     console.log(11);
    // })

    // arr.some(function(value) {
    //     if (value === 'green') {
    //         console.log('找到了该元素');
    //         return true;  // 在some里面遇到return true就是终止遍历,迭代效率更高
    //     }
    //     console.log(11);
    // })

    arr.filter(function(value) {
        if (value === 'green') {
            console.log('找到了该元素');
            return true;  // 在filter里面return不会终止迭代
        }
        console.log(11);
    })

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible"
        content="IE=edge">
    <meta name="viewport"
        content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        input {
            border: 0;
            outline: none;
        }

        table {
            margin: 44px auto;
        }

        th,
        td {
            height: 40px;
            text-align: center;
        }

        .search {
            width: 600px;
            margin: 16px auto;
        }

        .search input {
            width: 64px;
            height: 25px;
            border: 1px solid #c1b9c2;
        }

        .search button {
            width: 60px;
            height: 27px;
        }
    </style>
</head>

<body>
    <div class="search">
        <label for="start">按照价格查询: </label>
        <input type="text"
            id="start"> - <input type="text"
            class="end">
        <button class="search-price">搜索</button>
        <label for="product">按照商品名称查询: </label>
        <input type="text"
            id="product">
        <button class="search-pro">查询</button>
    </div>
    <table border="1"
        cellpadding="0"
        cellspacing="0"
        width="400">
        <thead>
            <tr>
                <th>id</th>
                <th>产品名称</th>
                <th>价格</th>
            </tr>
        </thead>
        <tbody>

        </tbody>
    </table>
</body>

</html>

<script>
    var data = [{
        id: 1,
        pname: '小米',
        price: 3999
    }, {
        id: 2,
        pname: 'oppo',
        price: 999
    }, {
        id: 3,
        pname: '荣耀',
        price: 1299
    }, {
        id: 4,
        pname: '华为',
        price: 1999
    },
    {
        id: 5,
        pname: '小米',
        price: 2699
    }];

    // 1. 获取相应的元素
    var tbody = document.querySelector('tbody');
    var search_price = document.querySelector('.search-price');
    var start = document.querySelector('#start');
    var end = document.querySelector('.end');
    var product = document.querySelector('#product');
    var search_pro = document.querySelector('.search-pro');

    setData(data);
    // 2. 将数据渲染到页面中
    function setData(myData) {
        // 先清空tbody里面的数据
        tbody.innerHTML = '';
        myData.forEach((item) => {
            var tr = document.createElement('tr');
            tr.innerHTML = `<td>${item.id}</td><td>${item.pname}</td><td>${item.price}</td>`;
            tbody.appendChild(tr);
        })
    }
    
    // 3. 根据价格查询商品
    search_price.addEventListener('click', function () {
        var newData = data.filter(function (value) {
            return value.price >= start.value && value.price <= end.value;
        })
        // 把筛选之后的对象渲染到页面中
        setData(newData)
    })

    // 4. 根据商品名称查找商品
    // 如果查询数组中唯一的元素,用some方法更合适,因为它找到这个元素,就不在进行循环,效率更高
    search_pro.addEventListener('click', function() {
        var arr = [];
        data.some(function (value) {
            if (value.pname === product.value) {
                // console.log(value);
                arr.push(value);
                // return true;  // return 后面必须写true
            }
        })
        setData(arr);
    })
</script>

3.3 字符串方法

trim() 方法会从一个字符串的两端删除空白字符。

	str.trim();

trim() 方法并不影响原字符串本身,它返回的是一个新的字符串。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text">
    <button>点击</button>
    <div></div>
</body>
</html>

<script>
    // trim方法去除两侧空白
    var str = '  andy  ';
    console.log(str);
    var str1 = str.trim();
    console.log(str1);

    var ipt = document.querySelector('input');
    var div = document.querySelector('div');
    var btn = document.querySelector('button');

    btn.addEventListener('click', function() {
        var str = ipt.value.trim();
        if (str === '') {
            alert('请输入内容')
        } else {
            console.log(str);
            console.log(str.length);
            div.innerHTML = str;
        }
    })
</script>

3.4 对象方法

  1. Object.keys() 用于获取对象自身所有的属性
	Object.keys(obj);
  • 效果类似for…in
  • 返回一个由属性名组成的数组
 var obj = {
        id: 1,
        pname: '小米',
        price: 2699,
        num: 2000
    };
    var arr = Object.keys(obj);
    console.log(arr);
    arr.forEach(function(value) {
        console.log(value);
    })
  1. Object.defineProperty() 定义对象中新属性或修改原有的属性。
	Object.defineProperty(obj, prop, descriptor)
  • obj:必须。目标对象
  • prop:必须。需定义或修改的属性的名字
  • descriptor:必须。目标属性所拥有的特性

Object.defineProperty() 第三个参数descriptor说明:以对象形式{ }书写

  • value:设置属性的值,默认为undefined
  • writable:值是否可以重写。true | false 默认为false
  • enumerable:目标属性是否可以被枚举(遍历)。true | false 默认为false
  • configurable:目标属性是否可以被删除或是否可以再次修改特性true | false 默认为false
// Object.defineProperty() 定义新属性或修改原有的属性
    var obj = {
        id: 1,
        pname: '小米',
        price: 2699,
    };

    // 1. 以前的对象添加和修改属性的方式
    // obj.num = 1000;
    // obj.price = 99;
    // 2. Object.defineProperty() 定义新属性或修改原有的属性
    Object.defineProperty(obj, 'num', {
        value: 1000,
        enumerable: true
    })
    console.log(obj);
    Object.defineProperty(obj, 'price', {
        value: 99
    })
    console.log(obj);
    Object.defineProperty(obj, 'id', {
        // 如果值为false,不允许修改这个属性值, 默认值为false
        writable: false
    })
    obj.id = 2;
    console.log(obj);
    Object.defineProperty(obj, 'address', {
        value: '中国山东蓝翔技校xx单元',
        // 如果值为false,不允许修改这个属性值, 默认值为false
        writable: false,
        // enumerable,如果值为false,则不允许遍历,默认的指是false
        enumerable: false,
        // configurable 如果为false,则不允许删除这个属性,也不允许修改第三个参数里面的特性,   默认为false
        configurable: false
    })
    console.log(Object.keys(obj));
    delete obj.address;
    console.log(obj);
    delete obj.pname;
    console.log(obj);
    Object.defineProperty(obj, 'address', {
        value: '中国山东蓝翔技校xx单元',
        // 如果值为false,不允许修改这个属性值, 默认值为false
        writable: true,
        // enumerable,如果值为false,则不允许遍历,默认的指是false
        enumerable: true,
        // configurable 如果为false,则不允许删除这个属性,也不允许修改第三个参数里面的特性,   默认为false
        configurable: true
    })

函数进阶

1. 函数的定义和调用

1.1 函数的定义方式

  1. 函数声明方式function关键字(命名函数)
  2. 函数表达式(匿名函数)
  3. new Function()
	var fn = new Function('参数1', '参数2'..., '函数体');
  • Function里面参数都必须是字符串格式
  • 第三种方式执行效率低,也不方便书写,因此较少使用
  • 所有函数都是Function的实例(对象)
  • 函数也属于对象
// 函数的定义方式

    // 1. 自定义函数(命名函数)
    function fn() {};

    // 2. 函数表达式(匿名函数)
    var fun = function() {};

    // 3. 利用 new Function('参数1', '参数2', '函数体');
    var f = new Function('a', 'b', 'console.log(a + b)');
    f(1, 2);
    // 4. 所有函数都是Function的实例(对象)
    console.dir(f);
    console.log(f instanceof Object);
Function.prototype
Function原型对象.constructor
f.__proto__
Function
构造函数
Function原型对象
f实例对象

1.2 函数的调用方式

  1. 普通函数
  2. 对象的方法
  3. 构造函数
  4. 绑定事件函数
  5. 定时器函数
  6. 立即执行函数
 // 函数的调用方式

    // 1. 普通函数
    function fn() {
        console.log('人生的巅峰');
    }
    // fn()  fn.call();
    // 2. 对象的方法
    var o = {
        sayHi: function() {
            console.log('人生的巅峰');
        }
    }
    o.sayHi();
    // 3. 构造函数
    function Star() {};
    new Star();
    // 4. 绑定事件函数
    // btn.onclick = function() {};  // 点击了按钮就可以调用这个函数
    // 5. 定时器函数
    // setInterval(function() {}, 1000);   // 这个函数是定时器每1秒钟自动调用一次
    // 6. 立即执行函数
    (function() {
        console.log('人生的巅峰');
    })();
    // 立即执行函数是自动调用的
    (function() {} ());
    (function() {}) ();

2.1 函数内this的指向

这些this的指向,是当我们调用函数的时候确定的。调用方式的不同决定了this的指向不同,一般指向调用者

调用方式this指向
普通函数调用window
构造函数调用实例对象 原型对象里面的方法也指向实例对象
对象方法调用该方法所属对象
事件绑定方法绑定事件对象
定时器函数window
立即执行函数window
	// 函数的不同调用方式决定了this的指向不同

    // 1. 普通函数 this指向window
    function fn() {
        console.log('普通函数的this:' + this);
    }
    fn()
    // 2. 对象的方法 this指向的是对象o
    var o = {
        sayHi: function() {
            console.log('对象的方法的this:' + this);
        }
    }
    o.sayHi();
    // 3. 构造函数 this指向zj这个实例对象,原型对象里面的this指向的也是zj这个实例对象
    function Star() {};
    Star.prototype.sing = function() {
        console.log(`原型对象里的this:${this}`);
    }
    var zj = new Star();
    zj.sing();
    // 4. 绑定事件函数 this指向的是函数的调用者btn这个按钮对象
    var btn = document.querySelector('button');
    btn.onclick = function() {
        console.log(`绑定事件函数的this:${this}`);
    };  
    // 5. 定时器函数 this指向的也是window
    setTimeout(function() {
        console.log(`定时器函数的this:${this}`);
    }, 1000); 
    // 6. 立即执行函数 this指向的还是window
    (function() {
        console.log(`立即执行函数的this:${this}`);
    })();

JavaScript为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部this的指向问题,常用的有bind()、call()、apply()三种方法。

1. call方法

call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向。

	fun.call(thisArg, arg1, arg2, ...)
 	// 1. call()
    var o = {
        name: 'andy'
    }
    
    function fn(a, b) {
        console.log(this);
        console.log(a + b);
    }
    fn.call(o, 10, 20)
    // call 第一可以调用函数,第二可以改变函数内的this指向
    // call的主要作用可以实现继承
    function Father(uname, age, sex) {
        this.uname = uname;
        this.age = age;
        this.sex = sex;
    }

    function Son(uname, age, sex) {
        Father.call(this, uname, age, sex);
    }
    var zj = new Son('张杰', 19, '男');
    console.log(zj);
2. apply方法

apply()方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的this指向。

	fun.apply(thisArg, [argsArray])
  • thisArg:在fun函数运行时指定的this值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数
 // 2. apply() 应用 运用的意思
    var o = {
        name: 'andy'
    }

    function fn(x, y) {
        console.log(this);
        console.log(x + y);
    }
    fn.apply(o, [44, 55])
    // 1. 也是调用函数 第二可以改变函数内部的this指向
    // 2. 但是它的参数必须是数组(伪数组)
    // 3. apply的主要应用,比如说我们可以利用apply借助于数学内置对象求最大值
    var arr = [1, 44, 55, 66, 99];
    var max = Math.max.apply(Math, arr);
    var min = Math.min.apply(Math, arr);
    console.log(max);
    console.log(min);
3. bind方法

bind()方法不会调用函数。但是能改变函数内部this指向

	fun.bind(thisArg, arg1, arg2, ...)
  • thisArg:在fun函数运行时指定的this值
  • arg1,arg2:传递的其他参数
  • 返回由指定的this值和初始化参数改造的原函数拷贝

2.2 call apply bind 总结

相同点:

都可以改变函数内部的this指向

区别点:
  1. call和apply会调用函数,并且改变函数内部this指向
  2. call和apply传递的参数不一样,call传递参数arg1,arg2…形式;apply必须数组形式[arg]
  3. bind不会调用函数,可以改变函数内部this指向
主要应用场景:
  1. call经常做继承
  2. apply经常跟数组有关系。比如借助于数学对象实现数组求最大值最小值
  3. bind不调用函数,但是还想改变this指向。比如改变定时器内部的this指向

3. 严格模式

3.1 什么是严格模式

JavaScript除了提供正常模式外,还提供了严格模式(strict mode)。ES5的严格模式是采用具有限制性JavaScript变体的一种方式,即在严格的条件下运行js代码。

严格模式再IE10以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。

  1. 消除了JavaScript语法的一些不合理、不严谨之处,减少了一些怪异行为。
  2. 消除代码运行的一些不安全之处,保证代码运行的安全。
  3. 提高编译器效率,增加运行速度。
  4. 禁用了在ECMAScript的未来版本中可能会定义的一些语法,为未来新版本的JavaScript做好铺垫。比如一些保留字如:class, enum, export, extends, import, super不能做变量名

3.2 开启严格模式

严格模式可以应用到整个脚本个别函数中。因此在使用h时,我们可以将严格模式分为为脚本开启严格模式为函数开启严格模式两种情况

1. 为脚本开启严格模式

为整个脚本文件开启严格模式,需要在所以语句之前放一个特定语句"use strict";(或 'use strict';)

	<script>
        'use strict';
        console.log('这是严格模式');
        // 下面的js代码就会安装严格模式执行代码
    </script>

因为"use strict"加了引号,所以老版本的浏览器会把它当作一行普通字符串而忽略。

有的script基本是严格模式,有的script脚本是正常模式,这样不利于文件合并,所以可以将整个脚本放在一个立即执行的匿名函数之中。这样独立创建一个作用域而不影响其他script脚本文件。

 	<script>
        (function() {
            'use strict';
            var num = 10;
            function fn() {};
        })();
    </script>
2. 为函数开启严格模式

要给某个函数开启严格模式,需要把"use strict";(或 'use strict';)声明放在函数体所有语句之前

 	<!-- 为某个函数开启严格模式 -->
    <script>
        function fn() {
            'use strict';
            // 下面的代码按照严格模式执行
        };
        function fun() {
            // 里面的还是按照普通模式执行
        };
    </script>

3.4 严格模式中的变化

1. 变量规定
  1. 在正常模式中,如果一个变量没有生命就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用var命令声明,然后再使用。
  2. 严禁删除已经声明的变量。例如,delete x;语法是错误的。
2. 严格模式下this指向问题
  1. 以前在全局作用域函数中的this指向window对象。
  2. 严格模式下全局作用域中函数中的this是undefined。
  3. 以前构造函数不加new也可以调用,当普通函数,this指向全局对象
  4. 严格模式下,如果构造函数不加new调用,this会报错
  5. new实例化的构造函数指向创建的对象实例
  6. 定时器this还是指向window
  7. 事件、对象还是指向调用者
3. 函数变化
  1. 函数不能有重名的参数
  2. 函数必须声明再顶层,新版本的JavaScript会引入"块级作用域"(ES6中已引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数。

更多严格模式要求参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Script_mode

4. 高阶函数

**高阶函数**是对其他函数进行操作的函数,它接收函数作为参数将函数作为返回值输出

	<script>
        function fn(callback) {
        	callback && callback();
    	}
		fn(function() {alert('hi')})
    </script>
	<script>
        function fn() {
        	return function() {}
    	}
    </script>

此时fn就是一个高阶函数

函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数。

5. 闭包

5.1 变量作用域

变量根据作用域的不同分为两种:全局变量和局部变量。

  1. 函数内部可以使用全局变量。
  2. 函数外部不可以使用局部变量。
  3. 当函数执行完毕,本作用域内的局部变量会销毁。

5.2 什么是闭包

闭包(closure)指有权访问另一个函数作用域中变量函数。 ------- JavaScript高级程序设计

简单理解就是,一个作用域可以访问另一个函数内部的局部变量。

	// 闭包(closure)指有权访问另一个函数作用域中变量的函数
    // 闭包: 我们fun这个函数作用域访问了另外一个函数fn里面的局部变量num
    // 我们fn外面的作用域可以访问fn内部的局部变量
    // 闭包的主要作用:延伸 了变量的作用范围
    function fn() {
        var num = 100;
        // function fun() {
        //     console.log(num);
        // }
        // return fun;
        return function() {
            console.log(num);
        }
    }
    var f = fn();
    f();
    // 类似于
    // var f = function fun() {
    //         console.log(num);
    //     }

5.5 闭包案例

  1. 循环注册点击事件
  2. 循环中的setTimeout()
  3. 计算打车价格
	// 闭合应用-闭合应用-点击li输出当前li的索引号
    // 1. 我们可以利用动态添加属性的方式
    var lis = document.querySelector('.nav').querySelectorAll('li');
    // for (var i = 0; i < lis.length; i++) {
    //     lis[i].index = i;
    //     lis[i].addEventListener('click', function() {
    //         console.log(this.index);
    //     })
    // }

    // 2. 利用闭包的方式得到当前小li的索引号
    for (var i = 0; i < lis.length; i++) {
        // 利用for循环创建了4个立即执行函数
        // 立即执行函数也称为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这个变量
        (function(i) {
            // console.log(i);
            lis[i].addEventListener('click', function() {
                console.log(i);
            })
        })(i);
    }
	// 闭包应用-闭包应用-3秒钟之后,打印所有li元素的内容
    var lis = document.querySelector('.nav').querySelectorAll('li');
    for (var i = 0; i < lis.length; i++) {
        (function(i) {
            setTimeout(() => {
                console.log(lis[i].innerHTML);
            }, 3000);
        })(i);
    }
	// 闭包应用-计算打车价格
    // 打车起步价13(3公里内), 之后每多一公里增加5块钱,用户输入公里数就可计算打车价格
    // 如果有拥堵情况,总价格多收取10块钱拥堵费
    var car = (function() {
        var start = 13;
        var total = 0;
        return {
            // 正常的总价
            price: function(n) {
                if (n <= 3) {
                    total = start;
                } else {
                    total = start + (n - 3) * 5;
                }
                return total;
            },
            // 拥堵之后的费用
            yd: function(flag) {
                return flag ? total + 10 : total;
            }  
        }
    })();
    console.log(car.price(5));
    console.log(car.yd(true));
    console.log(car.price(1));
    console.log(car.yd(false));

5.6 闭包总结

1. 闭包是什么?

闭包是一个函数(一个作用域可以访问另外一个函数的局部变量)

2. 闭包的作用是什么?

延伸变量的作用范围

6. 递归

6.1 什么是递归?

如果一个函数在内部可以调用其本身,那么这个函数就是递归函数

简单理解:函数内部自己调用自己,这个函数就是递归函数

递归函数的作用和循环效果一样

由于递归很容易发生"栈溢出"错误(stack overflow),所以必须要加退出条件return

6.2 利用递归求数学题

  1. 求 1 * 2 * 3 … *n
  2. 求斐波那契数列
	// 利用递归函数求1~n的阶乘 1 * 2 * 3 * 4 *  ..n
    function fn(n) {
        if (n == 1) {
            return 1;
        }
        return n * fn(n - 1);
    }   
    console.log(fn(4));
    console.log(fn(5));
    console.log(fn(1));
    // 详细思路 假如用户输入的是3
    // return 3 * fn(2);
    // return 3 * (2 * fn(1));
    // return 3 * (2 * 1);
    // return 3 * (2);
    // return 6;
	// 利用递归函数求斐波那契数列(兔子序列)  1、1、2、3、5、8、13、21...
    // 用户输入一个数字n就可以求出这个数字对应的兔子序列值
    // 我们只需要知道用户输入的n的前面两项(n -1 n -2)就可以计算出n对应的序列值
    function fb(n) {
        if (n == 1 || n == 2) {
            return 1;
        }
        return fb(n -1) + fb(n - 2);
    }   
    console.log(fb(4));
    // return fb(3) + fb(1);
    // return fb(2) + fb(1) + 1;
    // return 1 + 1 + 1;
    // return 3;

6.3 利用递归求:根据id返回对应的数据对象

 	var data = [{
        id: 1,
        name: '家电',
        goods: [{
            id: 11,
            gname: '冰箱'
        }, {
            id: 12, 
            gname: '洗衣机'
        }]
    }, {
        id: 2,
        name: '服饰'
    }]
    // 我们想要做输入id号,就可以返回的数据对象
    // 1. 利用forEach去遍历里面的每一个对象
    function inputId(json, id) {
        json.forEach(function(item) {
            // console.log(item);  // 两个数组元素
            if (item.id == id) {
                console.log(item);
                // 2. 我们想要得到里层是数据 11 12 可以利用递归函数
                // 里面应该有goods这个数组并且数组的长度不为0
            } else if (item.goods && item.goods.length > 0) {
                inputId(item.goods, id);
            }
        })
    }
    inputId(data, 1);
    inputId(data, 2);
    inputId(data, 12);

6.4 浅拷贝和深拷贝

  1. 浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用
  2. 深拷贝拷贝多层,每一级别的数据都会拷贝
  3. Object.assign(target, …sources) es6 新增方法可以浅拷贝
 	// 1. 浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用
    // 2. 深拷贝拷贝多层,每一级别的数据都会拷贝

    var obj = {
        id: 1,
        name: 'andy',
        msg: {
            age: 18
        }
    }

    // var o = {};
    // for (var k in obj) {
    //     o[k] = obj[k];
    // }
    // console.log(o);
    // o.msg.age = 20;
    // console.log(obj);

    console.log('-----------');
    Object.assign(o, obj);
    o.msg.age = 20;
    console.log(obj);
	// 深拷贝拷贝多层,每一级别的数据都会拷贝
    var obj = {
        id: 1,
        name: 'andy',
        msg: {
            age: 18
        },
        color: ['pink', 'bule', 'red']
    }

    var o = {};

    // 封装函数
    function deepCopy(newObj, oldObj) {
        for (var k in oldObj) {
            // 判断属性值属于那种数据类型
            // 1. 获取属性值 oldObj[k]
            var item = oldObj[k];
            // 2. 判断这个值是否是数组
            if (item instanceof Array) {
                newObj[k] = [];
                deepCopy(newObj[k], item);
            } else if (item instanceof Object) {
                // 3. 判断这个值是否是对象
                newObj[k] = {};
                deepCopy(newObj[k], item);
            } else {
                // 4. 属于简单数据类型
                newObj[k] = item;
            }
        }
    }
    deepCopy(o, obj);
    console.log(o);

    var arr = [];
    console.log(arr instanceof Object);
    o.msg.age = 20;
    console.log(obj);

正则表达式

1. 正则表达式概述

1.1什么是正则表达式

正则表达式(Regular Expression )是用于匹配字符串中字符组合的模式。在JavaScript中,正则表达式也是对象。

正则表达式通常用来检索、替换那些符合某个模式(规则)的文本,例如验证表单:用户名表单只能输入英文字母、数字或者下划线,昵称输入框中可以输入中午(匹配)。此外,正则表达式还常用于过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特点部分(提取)等。

其他语言也会使用正则表达式,本阶段我们主要是利用JavaScript正则表达式完成表单验证。

1.2 正则表达式的特点

  1. 灵活性、逻辑性和功能性非常的强。
  2. 可以迅速地用极简单的方式达到字符串的复杂控制。
  3. 对于刚接触的人来说,比较晦涩难懂。比如:^\w+([-+.]\w+)*@\w+([-.]\w+)*.\w+([-.]\w+)*$
  4. 实际开发,一般都是直接复制写好的正则表达式,但是要求会使用正则表达式并且根据实际情况修改正则表达式。比如用户名:/1{3,16}$/

2. 正则表达式在JavaScript中的使用

2.1 创建正则表达式

在JavaScript中,可以通过两种方式创建一个正则表达式。

1. 通过调用RegExp对象的构造函数创建
	var 变量名 = new RegExp(/表达式/);
2. 通过字面量创建
	var 变量名 = /表达式/;

// 注释中间放表达式就是正则字面量

2.2 测试正则表达式 test

test()正则对象方法,用于检测字符串是否符合该规则,该对象会返回true或者false,其参数是测试字符串。

	regexObj.test(str)
  1. regexObj是写的正则表达式
  2. str我们要测试的文本
  3. 就是检测str文本是否符合我们写的正则表达式规范
 // 正则表达式在js中使用

    // 1. 利用RegExp对象来创建正则表达式
    var regexp = new RegExp(/123/);
    console.log(regexp);

    // 2. 利用字面量创建正则表达式
    var rg = /123/;
    
    // 3. test方法用来检测字符串是否符合正则表达式要求的规范
    console.log(rg.test(123));
    console.log(rg.test('abc'));

3. 正则表达式中的特殊字符

3.1 正则表达式的组成

一个正则表达式可以由简单的字符构成,比如/abc/,也可以是简单和特殊字符的组合,比如/ab*c/。其中特殊字符也被称为元字符,在正则表达式中是具有特殊意义的专用符号,如^、$、+等。

特殊字符非常多,可以参考:

  • MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions
  • jQuery手册:正则表达式部分
  • 正则测试工具:https://tool.oschina.net/regex

3.2 边界符

正则表达式中的边界符(位置符)用来提示字符所处的位置,主要有两个字符。

边界符说明
^表示匹配行首的文本(以谁开始)
$表示匹配行尾的文本(以谁结束)

如果^和$在一起,表示必须是精确匹配。

 // 边界符 ^ $
    var rg = /abc/;  // 正则表达式里面不需要加引号,不管是数字型还是字符串型
    //   /abc/ 只要包含abc这个字符串返回的都是true
    console.log(rg.test('abc'));
    console.log(rg.test('abccc'));
    console.log(rg.test('dddabcaa'));
    console.log('------------------------');
    var reg = /^abc/;
    console.log(reg.test('abc'));
    console.log(reg.test('cabccc'));
    console.log(reg.test('dddabcaa'));
    console.log('--------------------');
    var reg1 = /^abc$/;  // 精确匹配,要求必须是abc字符串才符合规范
    console.log(reg1.test('abc'));
    console.log(reg1.test('abcabc'));

3.3 字符类

字符类表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内

1. 字符组合
	/^[a-z0-9]$/.test('a')     // true
2. [^]方括号内部取反符^
	/^[^abc]$/.test('a')     // false
 	// var rg = /abc/; 只要包含abc就可以了
    // 字符类: [] 表示有一系列字符可供选择,只要匹配其中一个就可以了
    var rg = /[abc]/;  // 只要包含有a或者包含有b或者包含有c,都返回为true
    console.log(rg.test('andy'));
    console.log(rg.test('bady'));
    console.log(rg.test('color'));
    console.log(rg.test('red'));
    console.log('--------------------');

    var rg = /^[abc]$/;  // 三选一,只有是a或者是b或者是c这三个字母才返回true
    console.log(rg.test('aa'));
    console.log(rg.test('a'));
    console.log(rg.test('b'));
    console.log(rg.test('c'));
    console.log(rg.test('abc'));
    console.log('-----------------');

    var reg = /^[a-z]$/;
    console.log(reg.test('w'));
    console.log(reg.test('r'));
    console.log(reg.test(1));
    console.log(reg.test('A'));
    console.log('------------------');

    var reg1 = /^[a-zA-Z0-9_-]$/;  // 26个英文字母(大写和小写都可以)任何一个字母返回true - 表示的是a到z的范围
    console.log(reg1.test('t'));
    console.log(reg1.test('Y'));
    console.log(reg1.test(9));
    console.log(reg1.test('-'));
    console.log(reg1.test('_'));

    // 如果中括号里面有^表示取反的意思,千万不要和边界符^混淆
    var reg2 = /^[^a-zA-Z0-9_-]$/;

3.4 量词符

量词符用来设定某个模式出现的次数

量词说明
*重复零次或更多次
+重复一次或更多次
重复零次或一次
{n}重复n次
{n,}重复n次或更多次
{n,m}重复n到m次
*?+???{m,}?{m,n}?使s, +, ?, {m,}, {m,n}变成非贪婪模式,也就是使这些匹配次数不定的表达式尽可能少的匹配
	// 量词符:用来设定某个模式出现的次数
    // 简单理解:就是让下面的a这个字符重复多少次
    // var reg = /^a*$/;
    // console.log(reg.test(''));
    // console.log(reg.test('a'));
    // console.log(reg.test('aaaaa'));

    // * 相当于 >= 0 可以出现0次或者很多次


    // + 相当于 >= 1 可以出现1次或者很多次
    // var reg = /^a+$/;
    // console.log(reg.test(''));
    // console.log(reg.test('a'));
    // console.log(reg.test('aaaaa'));


    // ? x相当于 1 || 0
    // var reg = /^a?$/;
    // console.log(reg.test(''));
    // console.log(reg.test('a'));
    // console.log(reg.test('aaaaa'));

    // {3} 就是重复3次
    // var reg = /^a{3}$/;
    // console.log(reg.test(''));
    // console.log(reg.test('a'));
    // console.log(reg.test('aaaaa'));
    // console.log(reg.test('aaa'));

    // {3, } 大于等于3
    // var reg = /^a{3,}$/;
    // console.log(reg.test(''));
    // console.log(reg.test('a'));
    // console.log(reg.test('aaaaa'));
    // console.log(reg.test('aaa'));

    // {3, 16} 大于等于3并且小于等于16
    var reg = /^a{3,16}$/;
    console.log(reg.test(''));
    console.log(reg.test('a'));
    console.log(reg.test('aaaaa'));
    console.log(reg.test('aaa'));
案例:用户名验证

功能需求:

  1. 如果用户名输入合法,则后面提示信息为:用户名合法,并且颜色为绿色
  2. 如果用户名输入不合法,则后面提示信息为:用户名不符合规范,并且颜色为红色
案例分析
  1. 用户名只能为英文字母,数字,下划线或者短横线组成,并且用户名长度为6~16位
  2. 首先准备好这种正则表达式模式/2{6,16}$/
  3. 当表单失去焦点就开始验证
  4. 如果符合正则规范,则让后面的span标签添加right类
  5. 如果不符合正则规范,则让后面的span标签添加wrong类
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .right {
            color: green;
        }

        .wrong {
            color: red;
        }
    </style>
</head>
<body>
    <input type="text" class="uname"> <span>请输入用户名</span>
</body>
</html>

<script>
    // 量词是设定某个模式出现的次数
    var reg = /^[a-zA-Z0-9_-]{6,16}$/; // 这个模式用户只能输入英文字母,数字,下划线,短横线但是有边界符和[],这就限定了只能多选1
    // console.log(reg.test('a'));
    // console.log(reg.test('A'));
    // console.log(reg.test(9));
    // console.log(reg.test(99));
    // console.log(reg.test('aaaa'));
    // console.log('---------------');
    // console.log(reg.test('andy-red'));
    // console.log(reg.test('andy_red'));
    // console.log(reg.test('andy009'));
    // console.log(reg.test('andy!009'));

    var uname = document.querySelector('.uname');
    var span = document.querySelector('span');
    uname.addEventListener('input', function() {
        if (reg.test(this.value)) {
            span.className = 'right';
            span.innerHTML = '用户名合法';
        } else {
            span.className = 'wrong';
            span.innerHTML = '用户名不符合规范';
        }
    })
</script>

3.5 括号总结

  1. 大括号 量词符。里面表示重复次数
  2. 中括号 字符集合。匹配方括号中的任意字符。
  3. 小括号表示优先级

可以在线测试:https://c.runoob.com/

3.6 预定义类

预定义类指的是某些常见模式的简写方式

预定类说明
\d匹配0-9之间的任意数字,相当于[0-9]
\D匹配所有0-9以外的字符,相当于[^0-9]
\w匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
\W除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
\s匹配空格(包括换行符、制表符、空格符等),相当于[\t\r\n\v\f]
\S匹配非空格的字符,相当于[^\t\r\n\v\f]
案例:表单验证
分析:
  1. 手机号码:/^1[3|4|5|7|8|9]\d{9}$/
  2. QQ:[1-9][0-9]{4,}(腾讯QQ号从10000开始)
  3. 昵称是中文:/3{2,8}$/
  4. 短信验证码:/^\d{6}$/
  5. 密码:/4{6,16}$/

4. 正则表达式中的替换

4.1 replace替换

replace()方法可以实现替换字符串操作,用来替换的参数可以是一个字符串或是一个正则表达式。

	srtingObject.replace(regexp/substr,replacement)
  1. 第一个参数:被替换的字符串或者正则表达式
  2. 第二个参数:替换为的字符串
  3. 返回是一个替换完毕的新字符串

4.2 正则表达式参数

	/表达式/[swich]

swich(也称为修饰符)按照什么样的模式来匹配,有三种值:

  • g:全局匹配
  • i:忽略大小写
  • gi:全局匹配+忽略大小写

4.3 分组(子模式)相关元字符

分组是指使用()包裹正则表达式中的某一个片段,将这一段正则表达式当成一个组来看待,分组不会对全局表达式结果产生影响,不过却能额外获得该分组的执行结果。分组主要用于提取数据。

符号含义
()分组符(子模式符),会捕获分组的内容
``
\n(n是整数)回溯引用,调用第n个分组的结果
?:表达式分组符内使用,非捕获分组,不在捕获对应分组的内容
	var preg = /\w+@(\w+.[a-z]{2,10})/;
	var result = preg.exec('[email protected]');
	// 分组内容会被提取出来,一并放到结果中
	console.log(result);
	// 结尾是com、cn、net中的任意一个才行
	var reg = /^\w+@\w.(com|cn|net)$/;
	var result = reg.exec('[email protected]');
	console.log(result);
		var reg = /<([a-z]+?)>\S+<\/\1>/;
	    var str = `
	        <p>
	            我是mm
	            <span>不是ss</span>    
	        </p>
	    `;
	    console.log(reg.exec(str));
	    console.log(reg.exec(str));
	
	    const regStyle = /<(style)>[\s\S]*<\/\1>/;
	    const str1 = `
	        <style>
	            .box {
	                color: #fff;
	            }    
	        </style>
	    `;
	
	 	 console.log(regStyle.exec(str1));

4.4 环视(断言)

如同^代表开头,$代表结尾,\b代表单词边界一样,断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为“零宽”。在处理匹配的时候,也是对当前位周围的情况进行观察,所以也叫“环视”。

符号含义
(?=表达式)正向先行断言,在某个位置向右看(写在右面),紧挨着该位置的右侧必须能匹配表达式
(?!表达式)反向先行断言,在某个位置向右看(写在右面),紧挨着该位置的右侧必须不能匹配表达式
(?<=表达式)正向后行断言,在某个位置向左看(写在左面),紧挨着该位置的左侧必须能匹配表达式
(?<!表达式)反向后行断言,在某个位置向左看(写在左面),紧挨着该位置的左侧必须不能匹配表达式
	// 正向先行断言
    // 匹配含有字符o的单词,但要求o的后面必须是个a,b,u中的一个
    var reg = /\w*o(?=[abu])\w*/;
    var str = 'hello world, I am your brother';
    var result = reg.exec(str);
    console.log(result); // your
	// 反向先行断言
    // 匹配含有字符o的单词,但要求o的右面不能是u
    var reg = /\w*o(?!u)\w*/;
    var str = 'hello world, I am your brother';
    var result = reg.exec(str);
    console.log(result);  // hello
	// 正向后行断言
    // 匹配含有字符o的单词,但要求o的左面必须是r
    var reg = /\w*(?<=r)o\w*/;
    var str = 'hello world, I am your brother';
    var result = reg.exec(str);
    console.log(result);  // brother	

关于先行(lookahead)和后行(lookbehind):

正则表达式引擎在执行字符串和表达式匹配时,会从头到尾(从前到后)连续扫描字符串中的字符,设想有一个扫描指针指向字符边界处并随匹配过程移动。先行断言,是当扫描指针位于某处时,引擎会尝试匹配指针还未扫过的字符,先于指针达到该字符,故称为先行,后行断言,引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。

作业:

字符串中必须含有至少一个大写字母、至少一个小写字母、至少一个数字,同时字符串的长度不能低于6位

	var rg = /(?=.*[A-Z])(?=.*[a-z]*)(?=.*[0-9]).{6,/;
    var str = 'sfagaaAg11';
    console.log(rg.exec(str));

ES6简介

1. 什么是ES6?

ES的全称是ECMAScript,它是由ECMA国际标准化组织,制定的一项脚本语言的标准化规范。

年份版本
2015年6月ES2015
2016年6月ES2016
2017年6月ES2017
2018年6月ES2018

ES6实际上是一个泛指,泛指ES2015及后续的版本。

1.1 为什么要使用ES6?

每一次标准的诞生都意味着语言的完整性,功能的加强。JavaScript语言本身也有一些令人不满意的地方。

  • 变量提升特性增加了程序运行时的不可预测性
  • 语法过于松散,实现相同的功能,不同的人可能会写出不同的代码

2. ES6新增声明变量的关键字

let

  • 声明的变量只在所处于的块级作用域有效
	if (true) {
        let a = 10;
    }
	console.log(a)	// a is not defined

注意:使用let关键字声明的变量才具有块级作用域,使用var声明的变量不具备块级作用域特性。

ES6中新增的用于声明变量的关键字。

  • 不存在变量提升
	console.log(a);	// a is not defined
	let a = 20;
  • 暂时性死区
	var tmp = 123;
	if (true) {
        tmp = 'abc';
        let tmp;
    }

经典面试题

	var arr = [];
	for (var i = 0; i < 2; i++) {
        arr[i] = function() {
            console.log(i);
        }
    }
	arr[0]();
	arr[1]();

经典面试题图解:此题的关键点在于变量i是全局的,函数执行时输出的都是全局作用域下的i值。

	let arr = [];
	for (let i = 0; i < 2; i++) {
        arr[i] = function() {
            console.log(i);
        }
    }
	arr[0]();
	arr[1]();

const

作用:声明常量,常量就是值(内存地址)不能变化的量。

  • 具有块级作用域
	if (true) {
        const a = 10;
    }
	console.log(a);	// a is not def
  • 声明常量时必须赋值
	const PI;	// Missing initializer in const declaration
  • 常量赋值后,值不能修改
	const PI = 3.14;
	PI = 100;	// Assignment to constant variable.
	const arr = [100, 200];
	arr[0] = 'a';
	arr[1] = 'b';
	console.log(arr);	// ['a', 'b'];
	arr = ['a', 'b'];	// Assignment to constant variable.

let、const、var的区别

  1. 使用var声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象
  2. 使用let声明的变量,其作用域为该语句所在的代码块内,不存在变量提升
  3. 使用const声明的是常量,在后面出现的代码中不能修改该常量的值
varletconst
函数级作用域块级作用域块级作用域
变量提升不存在变量提升不存在变量提升
值可更改值可更改值不可更改

ES6的新增语法

解构赋值

ES6中允许从数组中提取值,按照对应位置,对变量赋值。对象也可以实现解构。

数组解构

	let [a, b, c] = [1, 2, 3];
	console.log(a);
	console.log(b);
	console.log(c);

如果解构不成功,变量的值为undefined.

	let [foo] = [];
	let [bar, foo] = [1];

对象解构

	let person = {name: 'zhangsan', age: 20};
	let {name, age} = person;
	console.log(name);	// 'zhangsan'
	console.log(age);	// 20
	let person = {name: 'zhangsan', age: 20};
	let {name: maName, age: myAge} = person;	// myName myAge 属于别名
	console.log(myName);	// 'zhangsan'
	console.log(myAge);		// 20

箭头函数

ES6中新增的定义函数的方式

	() => {}
    const fn = () => {}

函数体重只有一句代码,且代码的执行结果就是返回值,可以省略大括号

	function sum(num1, num2) {
        return num1 + num2;
    }
	const sum = (num1, num2) => num1 + num2;

如果形参只有一个,可以省略小括号

	function fn(v) {
        return v;
    }
	const fn = v => v;

箭头函数不绑定this关键字,箭头函数中的this,指向的是函数定义位置的上下文this

	let obj = {
        name: 'andy',
        age: 20,
        say: () => {
            console.log(this.age);
        }
    }
    obj.say();

剩余参数

剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

	function sum(first, ...args) {
        console.log(first); // 10
        console.log(args);	// [20, 30]
    }
	sum(10, 20, 30);

剩余参数和解构配合使用

	let students = ['wangwu', 'zhangsan', 'lisi'];
    let [s1, ...s2] = students;
    console.log(s1);    // wangwu
    console.log(s2);    // ["zhangsan", "lisi"]

ES6的内置对象扩展

Array的扩展方法

扩展运算符(展开语法)

扩展运算符可以将数组或者对象转为用逗号分隔的参数序列。

	let arr = [1, 2, 3];
	...arr  // 1, 2, 3
	console.log(...arr); 	// 1 2 3

扩展运算符可以应用于合并数组

	// 方法一
	let arr1 = [1, 2, 3];
	let arr2 = [3, 4, 5];
	let arr3 = [...arr1, ...arr2];
	// 方法二
	arr1.push(...arr2);
	// 方法三
	let arr3 = arr1.concat(arr2);

将伪数组或可遍历对象转换为真正的数组

	let divs = document.querySelectorAll('div');
	divs = [...divs];
构造函数方法:Array.from()

将伪数组或可遍历对象转换为真正的数组

	let = arrayLike = {
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
    };
    let = arr = Array.from(arrayLike); 
    console.log(arr);   // ["a", "b", "c"]

方法还可以接收第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

	let = arrayLike = {
        '0': 'a',
        '1': 'b',
        length: 2
    };
	let newArr = Array.from(arrayLike, item => item * 2);

实例方法:find()

用于找出第一个符合条件的数组成员,如果没有找到返回undefined

	 let arr = [{
        id: 1,
        name: '张三'
    }, {
        id: 2,
        name: '李四'
    }];
    let target = arr.find((item, index) => item.id ==2);

实例方法:findIndex()

用于找出第一个符合条件的数组成员的位置,如果没有找到返回-1

	let arr = [1, 5, 10, 15];
	let index = arr.find((value, index) => value >9);
	console.log(index);	// 2

实例方法:includes()

表示某个数组是否包含给定的值,返回布尔值

	[1, 2, 3].includes(2)	// true
	[1, 2, 3].includes(4)	// false

String的扩展方法

模板字符串

ES6新增的创建字符串的方式,使用反引号定义。

	let name = `zhangsan`;
	let sayHello = `hello,my name is ${name}`;	// hello,my name is zhangsan

模板字符串中可以换行

	let result = {
        name: 'andy',
        age: 18,
        sex: '女'
    }

    let html = ` <div>
        <span>${result.name}</span>
        <span>${result.age}</span>
        <span>${result.sex}</span>
    </div>`;

在模板字符串中可以调用函数

	const sayHello = function() {
        return '哈哈哈 追不到我吧 我就是这么强大';
    };
	let = greet = `${sayHello()} 哈哈哈哈`;
	console.log(greet);  // 哈哈哈 追不到我吧 我就是这么强大 哈哈哈哈

实例方法:startsWith() 和 endsWith()

  • startsWith():表示参数字符串是否在原字符串的头部,返回布尔值
  • endsWith():表示参数字符串是否在原字符串的尾部,返回布尔值
	let str = 'Hello world!';
    str.startsWith('Hello');    // true
    str.endsWith('!')   // true

实例方法:repeat()

repeat方法表示将原字符串重复n次,返回一个新字符串

	'x'.repeat(3)	// 'xxx';
	'hello'.repeat(2)	// 'hellohello'

Set数据结构

ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值

Set本身是一个构造函数,用来生成Set数据结构

	const s = new Set();

Set函数可以接受一个数组作为参数,用来初始化

	const set = new Set([1, 2, 3, 4, 5]);

实例方法

  • add(value):添加某个值,返回Set结构本身
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功
  • has(value):返回一个布尔值,表示该值是否为Set的成员
  • clear():清除所有成员,没有返回值
	const s1 = new Set();
    s1.add(1).add(2).add(3);    // 向set结构中添加值
    s1.delete(2)                // 删除set结构中的2值
    s1.has(1)                   // 表示set结构中是否有1这个值 返回布尔值
    s.clear()                   // 清除set结构中的所有值      

遍历

Set结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值

	s.forEach(value => console.log(value));

  1. a-z0-9_- ↩︎

  2. a-zA-Z0-9_- ↩︎

  3. \u4e00-\u9fa5 ↩︎

  4. a-zA-Z0-9_- ↩︎

;