• JavaScript|JavaScript 高级语法——详细汇总


    JavaScript 高级语法

    目录

    一、变量提升和函数提升

    作用域的概念

    • 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。通俗的说,作用域:数据起作用的范围(某条数据可以在什么范围内使用)

    • JS使用var声明变量,以function来划分作用域,大括号"{}" 却限定不了var的作用域。用var声明的变量具有变量提升(declaration hoisting)的效果

      • 函数外面定义的变量是全局变量,函数可以直接使用

      • 在函数内部没有使用var定义的变量则为全局变量

      • 在函数内使用var关键字定义的变量是局部变量,即出了函数外边无法获取这个变量(作用域只限于函数内)

      • 在任何地方使用window全局对象来声明的变量(属性) 也是全局变量(全局对象的属性自然也是全局变量),如 window.test = 50

        在函数内用 var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问(以function为界,能访问本函数外的同名变量,不能访问同一层的其他函数内的同名变量

    • ES6里增加了 let 和 const,可以在{}, if, for里声明。用法同var,但作用域限定在块级,let声明的变量不存在变量提升

    ​ 具体参照 ES6语法内容(变量的作用域在ES6笔记中详细描述)

    JavaScript解析器会在自身作用域内将用var声明的变量和函数声明提前(hoist)

    通常JS引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将用var声明的变量声明函数声明提升至当前作用域的顶端,然后进行接下来的处理。(注:当前流行的JS引擎大都对源码进行了编译,由于引擎的不同,编译形式也会有所差异,我们这里说的预编译和提升其实是抽象出来的、易于理解的概念)

    • 变量的声明被提前到作用域顶部,赋值保留在原地

    • 函数声明整个"被提前"

    • 函数作为值赋给变量时只有变量“被提前”了,函数没有“被提前”

    建议:变量声明一定要放在作用域/函数的最上方(JavaScript 只有函数作用域!)

    1. 变量提升

    变量提升

    变量提升:JS引擎在预解析时会将变量声明(定义)提升到了当前作用域的顶部,初始值为undefined

    function hoistVariable() {
        if (!foo) {  // ????????? 此时 foo 为 undefined,也就是假
            var foo = 5;
        }
        console.log(foo); // 5
    }
    hoistVariable();  // 5
    // 运行代码,我们会发现foo的值是5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    经过预编译之后是这样的:

    // 预编译之后
    function hoistVariable() {
        var foo;
        if (!foo) {    // foo:undefined 只定义未赋值
            foo = 5;
        }
        console.log(foo); // 5
    }
    hoistVariable();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ② 变量提升后,与外界同名变量不会相互影响

    如果当前作用域中存在此变量声明,无论它在什么地方声明,引用此变量时就会在当前作用域中查找,不会去外层作用域查找了

    当然如果内部作用域都没有声明(定义)同名变量,就不存在变量提升,就会去外层找同名吧变量!

    例如:

    var foo = 3;
    function hoistVariable() {
        var foo = foo || 5;
        console.log(foo); // 5
    }
    hoistVariable();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    foo || 5这个表达式的结果是5而不是3,虽然外层作用域有个foo变量,但函数内是不会去引用的,因为经过预编译之后是这样的:

    // 预编译之后
    var foo = 3;
    function hoistVariable() {
        var foo;              // 经过变量提升,局部作用域函数内部已经有一个同名的foo变量了,就不会再到外层作用域查找
        foo = foo || 5;       // undefined || 5  => 5
        console.log(foo); // 5
    }
    hoistVariable();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ③ 多次声明变量

    如果当前作用域中声明了多个同名变量,它们的同一个标识符会被提升至作用域顶部,其他部分按顺序执行

    function hoistVariable() {
        var foo = 3;
        {
            var foo = 5;
        }
        console.log(foo); // 5
    }
    hoistVariable();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但是由于JavaScript的 var 定义的变量没有块作用域,只有全局作用域和函数作用域,所以经过预编译之后是这样的:

    // 预编译之后
    function hoistVariable() {
        var foo;
        foo = 3;
        {
            foo = 5;
        }
        console.log(foo); // 5
    }
    hoistVariable();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2. 函数提升

    在定义一个函数的时候通常有两种声明方式:

    function foo(){};   // 函数声明
    var foo = function(){};  // 函数表达式
    
    • 1
    • 2

    不同之处

    1、函数声明会提前预解析(function声明的函数,包括函数体,整个提前,但只是位置提前,函数体不会被执行解析)

    2、函数表达式后面加括号可以直接执行,但不存在预解析(不能被提升)

    函数提升

    函数提升指的是函数声明的提升,JS引擎在预解析时会将整个函数声明代码块提升到当前作用域的顶部,原先位置已不存在该代码

    注意:函数声明提升的优先级大于变量提升

    foo(); // output: I am hoisted
    
    function foo() {
        console.log('I am hoisted');
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    要解决这个问题,我们先要弄清楚JavaScript解析器的工作机制

    也就是说,上面的例子其实被解析器理解解析成了以下形式:

    function foo() {
        console.log('I am hoisted');
    }
    
    foo(); // output: I am hoisted
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ② 函数表达式不存在函数提升(函数表达式,那就是变量提升方式)

    foo();     // 函数声明
    foo_later();   // foo_later is not a function
    
    function foo(){ console.log('函数声明'); }
    var foo_later = function(){ console.log('函数表达式'); }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到,函数声明foo被预解析了,它可以在其自身代码之前执行;而函数表达式foo_later则不能。

    经过预编译之后是这样的:

    function foo(){ console.log('函数声明'); }  // 函数声明全部被提前
    var foo_later;   // 函数表达式(变量声明)仅将变量提前,赋值操作没有被提前
    
    foo();       
    foo_later();   
    
    
    foo_later = function(){ console.log('函数表达式'); }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这样也就可以解释,为什么在函数表达式之前调用函数,会返回错误了,因为它还没有被赋值,只是一个未定义变量,当然无法被执行

    ③ 同名的函数表达式会覆盖函数声明

    在执行过程中,由于函数声明以及本身在编译时被提升到当前作用域的顶端,遇到同名的函数表达式,则函数表达式会覆盖函数声明

    覆盖关系与位置无关,因为无论函数声明在前还是函数表达式在前,函数声明总是被提升到最顶端,因此总是函数声明被覆盖!

    实例1

    var test = function () {
        console.log('函数表达式=', 111);
    }
    function test() {
        console.log('函数声明=', 222)
    }
    test();    // 函数表达式=111
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    经过预编译之后是这样的:

    var test
    test = function test() {  // 这里的第二个test只是一个名字,没啥用,取的和test不一样都行
        console.log('函数声明=', 222)
    }
    test = function () {  // 覆盖了函数声明的内容
        console.log('函数表达式=', 111);
    }
    test();    // 函数表达式=111
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    实例2

    function hoistFunction() {
        foo(); // 2
        var foo = function() {
            console.log(1);
        };
        foo(); // 1
        function foo() {
            console.log(2);
        }
        foo(); // 1
    }
    hoistFunction();
    // 输出的结果依次是2 1 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    因为JavaScript中的函数是一等公民,函数声明的优先级最高,会被提升至当前作用域最顶端,所以第一次调用时实际执行了下面定义的函数声明,然后第二次调用时,由于前面的函数表达式与之前的函数声明同名,故将其覆盖,以后的调用也将会打印同样的结果。上面的过程经过预编译之后,代码逻辑如下:

    // 预编译之后
    function hoistFunction() {
        var foo;
        foo = function foo() {
            console.log(2);
        }
        foo(); // 2
        foo = function() {
            console.log(1);
        };
        foo(); // 1
        foo(); // 1
    }
    hoistFunction();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ④ 函数和变量重名

    明白了变量提升和函数提升的优先级后,也不难理解,函数声明和变量重名时,变量会覆盖函数声明

    var foo = 3;
    
    function hoistFunction() {
        console.log(foo); // function foo() {}
        foo = 5;          // 覆盖了函数声明
        console.log(foo); // 5
        function foo() {}
    }
    
    hoistFunction();
    console.log(foo);   // 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到,函数声明被提升至作用域最顶端,然后被赋值为5,而外层的变量并没有被覆盖,经过预编译之后,上面代码的逻辑是这样的:

    // 预编译之后
    var foo = 3;
    
    function hoistFunction() {
        var foo;
        foo = function foo() {};
        console.log(foo); // function foo() {} 
        foo = 5;
        console.log(foo); // 5
    }
    
    hoistFunction();
    console.log(foo);  // 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    所以,再次证明,函数声明的优先权是最高的,它永远被提升至当前作用域最顶部,然后才是变量提升,这一点要牢记。

    ⑤ 声明函数注意点

    由于函数声明会被预解析,所以不要使用此种方法来声明不同函数!

    if(true){
        function fn(){
            alert('1');
        } 
    }
    else{
        function fn(){
            alert('2');
        }
    }
    
    fn();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    与我们预想的不同,该段代码弹出的是"2"!

    这是因为两个函数声明在if语句被执行之前就被预解析了,所以if语句根本没有用,调用fn()的时候直接执行了第二个函数

    二、 构造函数和原型

    1. 利用构造函数创建对象

    创建对象的方法(ES6之前)

    // 1. 利用 new Object() 创建对象
    var obj1 = new Object();
    
    // 2. 利用 对象字面量 创建对象
    var obj2 = {};
    
    // 3. 利用构造函数创建对象  (ES5)
    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
        this.sing = function() {
            console.log('我会唱歌');
        }
    }
    
    var ldh = new Star('刘德华', 18);
    var zxy = new Star('张学友', 19);
    ldh.sing();
    zxy.sing();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2. 静态成员和实例成员

    构造函数中的属性和方法我们称为成员, 成员可以添加
    1.实例成员就是构造函数内部通过this添加的成员 uname、age、sing 就是实例成员
    实例成员只能通过实例化的对象来访问
    2.静态成员就是在构造函数本身上添加的成员 sex 就是静态成员
    静态成员只能通过构造函数来访问

    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
        this.sing = function() {
            console.log('我会唱歌');
        }
    }
    var ldh = new Star('刘德华', 18);
    
    console.log(ldh.uname);  // 实例成员只能通过实例化的对象来访问
    ldh.sing();
    // console.log(Star.uname); // 不可以通过构造函数来访问实例成员
    
    Star.sex = '男';
    console.log(Star.sex);  // 静态成员只能通过构造函数来访问
    console.log(ldh.sex); // 不能通过对象来访问
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3. 原型

    ① 构造函数的原型对象(显式原型)

    原型对象实现方法的共享,不必开辟多余的内存空间
    一般情况下,公共属性定义到构造函数里面, 公共的方法放到原型对象身上

    // 1. 构造函数的问题
    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
        // this.sing = function() {
        //     console.log('我会唱歌');
        // }
    }
    Star.prototype.sing = function() {  // 原型对象实现方法的共享,不必开辟多余的内存空间
    	console.log('我会唱歌');
    }
    var ldh = new Star('刘德华', 18);
    var zxy = new Star('张学友', 19);
    console.log(ldh.sing === zxy.sing);  // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ② 实例对象的原型对象(隐式原型) __ proto __

    系统自己在对象身上添加一个 __proto__ 指向我们构造函数的原型对象 prototype
    方法的查找规则: 首先先看ldh 对象身上是否有 sing 方法,如果有就执行这个对象上的 sing
    如果没有 sing 这个方法,因为有__proto__ 的存在,就去构造函数原型对象prototype身上去查找sing这个方法

    原型链获取数据两种情况:
    引用类型的对象:他们的字面量__proto__ 等于他们类型的构造函数的 prototype
    通过 new 构造函数 的prototype等于这个构造函数创建的实例对象的__proto__

    请添加图片描述

    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
    }
    Star.prototype.sing = function() {
        console.log('我会唱歌');
    }
    var ldh = new Star('刘德华', 18);
    console.log(ldh.__proto__ === Star.prototype);  // true 实例对象的原型对象和构造函数的原型对象是同一个
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ③ 手动指回构造函数

    很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数

    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
    }
    // 此种方式添加新方法不会覆盖原先的内容知识添加新的
    // Star.prototype.sing = function() {
    //     console.log('我会唱歌');
    // };
    // Star.prototype.movie = function() {
    //     console.log('我会演电影');
    // }
    // 此种方式添加新方法直接覆盖了原来的对象内容,要想使用原来的,必须把构造函数指回原来的构造函数
    Star.prototype = {
        // 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象(覆盖),则必须手动的利用constructor指回原来的构造函数
        constructor: Star,
        sing: function() {
        	console.log('我会唱歌');
        },
        movie: function() {
        	console.log('我会演电影');
        }
    }
    var ldh = new Star('刘德华', 18);
    console.log(Star.prototype);
    console.log(ldh.__proto__);
    console.log(Star.prototype.constructor);
    console.log(ldh.__proto__.constructor);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    ④ 原型链

    原型属性
    读取对象的属性值时,会自动去原型链找
    设置对象的属性值时,不会查找原型链,如果当前对象中没有此属性,直接添加属性并且进行赋值
    方法一般定义在原型中,属性一般通过构造函数定义在实例对象本身上
    Object 对象是所有JS对象的父级对象
    hasOwnProperty
    用来判断某个属性是否属于实例对象,还是继承与构造函数
    这个方法不会检测对象的原型链,只会检测对象本身

    function Star(uname, age) {
        this.uname = uname;
        this.age = age;
    }
    Star.prototype.sing = function() {
    	console.log('我会唱歌');
    }
    var ldh = new Star('刘德华', 18);
    // 1.只要是对象就有__proto__ 原型, 指向原型对象
    console.log(Star.prototype.__proto__ === Object.prototype);
    // 2.Star原型对象里面的__proto__原型指向的是 Object.prototype
    console.log(Object.prototype.__proto__);
    // 3.Object.prototype原型对象里面的__proto__原型  指向为 null
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    hasOwnProperty 方法

    function Person (uname,age){
        this.uname  = uname;
        this.age = age;
    }
    Person.prototype.sex = '女';
    var p1 = new Person('张三',20);
    p1.phone = 123456;
    console.log(p1);
    console.log(p1.hasOwnProperty('phone')); // true
    console.log(p1.hasOwnProperty('uname')); // true
    console.log(p1.hasOwnProperty('sex'));  // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ⑤ 构造函数和原型对象中的this指向

    function Star(uname, age) {  // 1.在构造函数中,里面this指向的是对象实例 ldh
        this.uname = uname;
        this.age = age;
    }
    var that;
    Star.prototype.sing = function() {  // 2.原型对象的函数里面的this 指向的是 实例对象 ldh
        console.log('我会唱歌');
        that = this;
    }
    var ldh = new Star('刘德华', 18);  // this 指向 ldh
    
    ldh.sing();
    console.log(that === ldh);  // ture  // this 指向 ldh
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ⑥ 原型对象扩展内置方法

    // 原型对象的应用 扩展内置对象方法
    Array.prototype.sum = function() {
        var sum = 0;
        for (var i = 0; i < this.length; i++) {
            sum += this[i];
        }
        return sum;
    };
    // Array.prototype = {   // 不可以用这种方扩展内置对象的方法,不被允许,换了也白换!
    //     sum: function() {
    //         return sum;
    //     }
    // }
    var arr = [1, 2, 3];
    console.log(arr.sum());
    var arr1 = new Array(11, 22, 33);
    console.log(arr1.sum());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    改变函数内this指向 js提供了三种方法 call()、apply()、bind()

    • call()、apply()方法同时也是函数调用的方法,修改this,调用函数
    • bind()不会调用函数,会返回原函数改变this之后产生的新函数

    ⑦ call()

    (1) 改变 this 指向

    fn.call(obj,参数1,参数2…)
    1.call() 可以调用函数
    2.call() 可以改变这个函数的this指向 此时这个函数的this 就指向了obj这个对象

    // call 方法
    function fn(x, y) {
        console.log(this);  // obj
        console.log(x + y);
    }
    var obj = {
        name: 'andy'
    };
    fn.call(obj, 1, 2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    (2) 借用父类构造函数继承属性

    通过call()改变父类构造函数的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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    (3) 借用原型对象继承方法
    // 借用父构造函数继承属性
    // 1.父构造函数
    function Father(uname, age) {
        this.uname = uname;
        this.age = age;
    }
    Father.prototype.money = function() {
        console.log(100000);
    };
    // 2.子构造函数 
    function Son(uname, age, score) {
        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);  // 有了 money 这个方法
    console.log(Father.prototype);  // 不受影响
    console.log(Son.prototype.constructor);  // 指向自己的构造函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    ⑧ apply()

    apply() 需要在括号内的对象里面用数组包裹起来
    1.也是调用函数,可以改变函数内部的this指向
    2.但是他的参数必须是数组(伪数组)
    3.apply 的主要应用: 可以利用 apply 借助于数学内置对象求数组最大值

    var obj = {
        name: 'andy'
    };
    
    function fn(arr) {
        console.log(this); // obj
        console.log(arr); // 'pink'
    };
    fn.apply(obj, ['pink']);
    
    var arr = [1, 66, 3, 99, 4];
    var arr1 = ['red', 'pink'];
    // var max = Math.max.apply(null, arr);
    var max = Math.max.apply(Math, arr);
    var min = Math.min.apply(Math, arr);
    console.log(max, min);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    关于apply()小技巧
    // 原本Math.min()只能一个一个传数值,比较大小,不能放数组的形式比较大小
    // 可以巧用apply()方法,不用管this指向,跟这个无关,用了apply(),第二个参数就是数组了
    console.log(Math.min(1, 2, 3, 4, 5, 6));
    var arr = [1, 2, 3, 4, 5, 6, 7, 8];
    console.log(Math.min.apply(null, arr));
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ⑨ bind()

    bind() 绑定、捆绑的意思,预设this指向
    1.不会调用原来的函数 可以改变原来函数内部的this 指向
    2.返回的是原函数改变this之后产生的新函数
    3.如果有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向此时用 bind()

    // 3. bind()  绑定 捆绑的意思
    var obj = {
        name: 'andy'
    };
    
    function fn(a, b) {
        console.log(this);
        console.log(a + b);
    };
    var f = fn.bind(obj, 1, 2);
    f();
    
    // 4. 我们有一个按钮,当我们点击了之后,就禁用这个按钮,3秒钟之后开启这个按钮
    // var btn1 = document.querySelector('button');
    // btn1.onclick = function() {
    //     this.disabled = true; // 这个this 指向的是 btn 这个按钮
    //     // var that = this;
    //     setTimeout(function() {
    //         // that.disabled = false; // 定时器函数里面的 this 指向的是window
    //         this.disabled = false; // 由于bind(),此时定时器函数里面的 this 指向的是btn
    //     }.bind(this), 3000); // 这个this 指向的是btn 这个对象
    // }
    var btns = document.querySelectorAll('button');
    for (var i = 0; i < btns.length; i++) {
        btns[i].onclick = function() {
            this.disabled = true;
            setTimeout(function() {
                this.disabled = false;
            }.bind(this), 2000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    call()、apply()、bind()总结

    请添加图片描述

    三、对象

    1. 对象方法

    Object.keys()

    • 用于获取对象自身所有的属性名

    请添加图片描述

    // Object.keys() 用于获取对象自身所有的属性名
    var obj = {
        id: 1,
        pname: '小米',
        price: 1999,
        num: 2000
    };
    var arr_keys = Object.keys(obj);
    console.log(arr_keys);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Object.defineProperty()

    • 用于定义新属性或修改原有的属性
    • 格式: Object.defineProperty(目标对象,属性名,descriptor)

    请添加图片描述

    // Object.defineProperty() 定义新属性或修改原有的属性
    var obj = {
        id: 1,
        pname: '小米',
        price: 1999
    };
    // 1. 以前的对象添加和修改属性的方式
    // obj.num = 1000;
    // obj.price = 99;
    // console.log(obj);
    // 2. Object.defineProperty() 定义新属性或修改原有的属性
    Object.defineProperty(obj, 'num', {
        value: 1000,
        enumerable: true
    });
    console.log(obj);
    Object.defineProperty(obj, 'price', {
        value: 9.9
    });
    console.log(obj);
    
    // writable:是否可以修改,默认false不可以修改
    Object.defineProperty(obj, 'id', {
        // 如果值为false 不允许修改这个属性值 默认值也是false
        writable: false,
    });
    obj.id = 2; // 不报错,但也改不了
    console.log(obj);
    
    // enumerable:是否允许遍历,如果值为false 则不允许遍历, 默认的值是 false
    // configurable:是否允许删除这个属性/收复允许修改第三个参数里面的特性,如果为false 则不允许删除这个属性 不允许再修改第三个参数里面的特性 默认为false
    Object.defineProperty(obj, 'address', {
        value: '中国山东蓝翔技校xx单元',
        // 如果只为false 不允许修改这个属性值 默认值也是false
        writable: false,
        // enumerable 如果值为false 则不允许遍历, 默认的值是 false
        enumerable: false,
        // configurable 如果为false 则不允许删除这个属性 不允许再修改第三个参数里面的特性 默认为false
        configurable: false
    });
    console.log(obj);
    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
    });
    console.log(obj.address); // 不允许设置第三个参数中的特性了
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    注意:只有用defineProperty方法新添加出来的属性的那三个默认值才为false!原有的不是用这个方法定义的属性那三个默认值不为false!

    2. 浅拷贝和深拷贝

    • 浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用(拷贝一层内容,深层只拷贝地址)
    • 深拷贝拷贝多层, 每一级别的数据都会拷贝(拷贝所有内容)

    ① 浅拷贝

    // 浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用((拷贝一层内容,深层只拷贝地址)
    var obj = {
        id: 1,
        name: 'andy',
        msg: {
            age: 18
        }
    };
    var o = {};
    for (var k in obj) {
       // k 是属性名   obj[k] 是属性值
       o[k] = obj[k];
    }
    console.log(o);
    o.msg.age = 20;
    console.log(obj);  // obj 的 msg 的 age 也被改了
    
    console.log('--------------');
    Object.assign(o, obj);// 浅拷贝的语法糖(ES6的)
    console.log(o);
    o.msg.age = 20;
    console.log(obj);  // obj 的 msg 的 age 也被改了
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ② 深拷贝

    // 深拷贝拷贝多层, 每一级别的数据都会拷贝(拷贝所有内容)
    var obj = {
        id: 1,
        name: 'andy',
        msg: {
            age: 18
        },
        color: ['pink', '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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    四、函数

    ① 函数的调用方式

    • 普通函数调用
    • 对象方法的调用
    • 构造函数的调用
    • 绑定事件函数的调用
    • 定时器的调用
    • 立即执行函数的调用
    • call()、apply()、bind()调用函数:点击前往
    // 1. 普通函数
    function fn() {
        console.log('人生的巅峰');
    
    }
    fn();
    // 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('人生的巅峰');
    })();  // 立即执行函数是自动调用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    ② 不同函数的 this 指向

    函数的不同调用方式决定了this 的指向不同

    // 1. 普通函数 this 指向window
    function fn() {
        console.log('普通函数的this' + this);
    }
    window.fn();
    
    // 2. 对象的方法 this指向的是对象 o
    var o = {
        sayHi: function() {
            console.log('对象方法的this:' + this);
        }
    }
    o.sayHi();
    
    // 3. 构造函数 this 指向 ldh 这个实例对象 原型对象里面的this 指向的也是 ldh这个实例对象
    function Star() {};
    Star.prototype.sing = function() {
    
    }
    var ldh = new Star();
    
    // 4. 绑定事件函数 this 指向的是函数的调用者 btn这个按钮对象
    var btn = document.querySelector('button');
    btn.onclick = function() {
        console.log('绑定时间函数的this:' + this);
    };
    
    // 5. 定时器函数 this 指向的也是window
    window.setTimeout(function() {
        console.log('定时器的this:' + this);
    
    }, 1000);
    
    // 6. 立即执行函数 this还是指向window
    (function() {
        console.log('立即执行函数的this' + this);
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    ③ 高阶函数

    高阶函数:英文叫Higher-order function。JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数

    简单来说,所谓高阶函数,就是函数中可以传入另一个函数作为参数的函数

    高阶函数-函数可以作为参数传递
    function fn(a, b, callback) {
        console.log(a + b);
        callback && callback();
    }
    fn(1, 2, function() {
        console.log('我是最后调用的');
    
    });
    $("div").animate({
        left: 500
    }, function() {
        $("div").css("backgroundColor", "purple");
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ④ 递归函数

    递归函数应用

    • 利用递归查询任意层级的数据
    <script>
        var data = [{
            id: 1,
            name: '家电',
            goods: [{
                id: 11,
                gname: '冰箱',
                goods: [{
                    id: 111,
                    gname: '海尔'
                }, {
                    id: 112,
                    gname: '美的'
                }, ]
            }, {
                id: 12,
                gname: '洗衣机'
            }]
        }, {
            id: 2,
            name: '服饰'
        }];
        // 我们想要做输入id号,就可以返回的数据对象
        // 1. 利用 forEach 去遍历里面的每一个对象
        function getID(json, id) {
            var o = {};
            json.forEach(function (item) {
                // console.log(item); // 2个数组元素
                if (item.id == id) {
                    // console.log(item);
                    o = item;
                    // 2. 我们想要得里层的数据 11 12 可以利用递归函数
                    // 里面应该有goods这个数组并且数组的长度不为 0 
                } else if (item.goods && item.goods.length > 0) {
                    o = getID(item.goods, id);
                }
    
            });
            return o;
        }
        console.log(getID(data, 1));
        console.log(getID(data, 2));
        console.log(getID(data, 11));
        console.log(getID(data, 12));
        console.log(getID(data, 111));
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    五、严格模式

    ① 严格模式的使用方法

    
    <script>
        'use strict';
        //   下面的js 代码就会按照严格模式执行代码
    script>
    <script>
        (function() {
            'use strict';
        })();
    script>
    
    
    
    <script>
        // 此时只是给fn函数开启严格模式
        function fn() {
            'use strict';
            // 下面的代码按照严格模式执行
        }
    
        function fun() {
            // 里面的还是按照普通模式执行
        }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    ② 严格模式的注意点

      1. 变量必须先声明再使用
      2. 不能随意删除已经声明好的变量
      3. 严格模式下全局作用域中函数中的 this 指向的是 undefined
      4. 严格模式下,如果构造函数不加 new 调用, this 指向的是undefined,如果给this添加属性则会报错
      5. 定时器 this 还是指向 window
      6. 严格模式下函数里面的参数不允许有重复名
    <script>
        'use strict';
        // 1. 我们的变量名必须先声明再使用
        // num = 10;
        // console.log(num);  // 报错
        var num = 10;
        console.log(num);
        
        // 2. 不能随意删除已经声明好的变量
        // delete num;
        
        // 3. 严格模式下全局作用域中函数中的 this 指向的是 undefined
        function fn() {
             console.log(this); // undefined。
        }
        fn();
        
        // 4. 严格模式下,如果构造函数不加new调用, this 指向的是 undefined,如果给this添加属性则会报错
        function Star() {
            this.sex = '男';
        }
        Star();
        var ldh = new Star();
        console.log(ldh.sex);
        
        // 5. 定时器 this 还是指向 window 
        setTimeout(function() {
            console.log(this);
        }, 2000);
        
        // 6. 严格模式下函数里面的参数不允许有重复名
        function fn(a, a) {
            console.log(a + a);
        };
        fn(1, 2);
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    六、闭包

    ① 闭包的定义

    • 理解一:闭包是嵌套的内部函数(绝大部分人)
    • 理解二:闭包是包含被引用变量(函数)的对象(极少数人)
    // 闭包(closure)指有权访问另一个函数作用域中变量的函数
    // 闭包: 我们fun 这个函数作用域 访问了另外一个函数 fn 里面的局部变量 num
    function fn() {
        var num = 10;  // num 存在于闭包中
        var age = 20;  // age 不在闭包中,因为内部函数没调用这个外部的局部变量
        function fun() {
            console.log(num);
        }
        fun();
        // return fun()
    }
    fn();
    
    // 一般这么使用
    function fn() {
         var num = 10;
         return function() {
             console.log(num);
         }
     }
    let f = fn()
    f()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ② 产生闭包的条件

    • 1.函数嵌套
    • 2.内部函数引用了外部函数的数据(变量或者函数)
    • 3.外部函数执行

    产生闭包:当一个内部函数引用外部函数中变量或者函数的时候,闭包就产生了

    ③ 闭包的生命周期

    • 闭包产生:在嵌套的内部函数定义执行完之后就产生了(不是在调用的时候产生,在变量提升之前一点产生)
    • 闭包死亡:在嵌套的内部函数成为垃圾对象之后,闭包死亡
    function fun1() {
        // 闭包产生了(闭包是函数提升的时候产生的,内部函数对象已经创建了)
        var a = 10;
    
        function fun2() {
            a++;
            console.log(a);
        }
    
        function fun3() {
            a--;
            console.log(a);
        }
        return fun3;  // fun2成为了一个垃圾对象,没有用到
    }
    var f = fun1();
    f();
    f = null; // 闭包死亡
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    注意外部函数调用后,嵌套的内部函数定义执行完之后(函数提升)就产生了,不是需要内部函数执行才产生闭包!
    请添加图片描述

    请添加图片描述

    由图可知,此时程序即将执行var num = 10;这条语句时,闭包已经产生!

    由于存在变量提升、函数提升,此时变量num和函数fun已经被提升,也就是内部函数fun已经定义完成 ,此时就产生了闭包!

    而不是在调用内部函数fun的时候闭包才产生!

    请添加图片描述

    此时,外部函数的num已经完成赋值,闭包中的num也就获取到了外部num的数值

    ④ 闭包的作用

    • 1.使函数内部的变量在函数执行完之后,仍然存活在内存中(延长局部变量的生命周期)
    • 2.让函数外部可以操作到函数内部的数据(变量或函数)

    ⑤ 闭包的缺点

    • 函数执行完之后,函数内部局部变量没有释放。占用内存时间会变长
    • 容易造成内存泄露

    内存溢出和内存泄露

    内存溢出

    一种程序错误
    当程序运行需要的内存超过了剩余的内存的时候,抛出内存溢出的错误
    
    • 1
    • 2

    内存泄露

    占用的内存没有及时释放
    内存泄露积累多了容易导致内存溢出
    
    • 1
    • 2

    常见的内存泄露
    意外的全局变量
    没有及时清理的定时器
    闭包

    ⑥ 常见的闭包

    // ① 将内部函数作为外部函数的返回值
    function fun1() {
        var a = 10;
        function fun2() {
            a++;
            console.log(a);
        }
        return fun2
    }
    
    // 一般直接返回一个匿名函数即可
    function fn() {
         var num = 10;
         return function() {
             console.log(num);
         }
     }
    // 如果不存在引用指向(只调用了外部函数,内部函数根本没执行),那么闭包就会消失(虽然产生了闭包,但没意义)
    /* 
                fun1();  // 11
                fun1();  // 11
                这样闭包就没意义
    */
    var f = fun1()  // 此时是把fun2 赋值给了f,f是一个函数,相当于:  var f = fun2;
    f();  // 11
    f();  // 12  // 基于上一次的值继续执行,延长了变量的生命周期连续调用会基于上一次的值继续执行,延长了外部函数的局部变量的生命周期
    // 或者 fun1()()
    
    // ② 将函数作为实参传递给另一个函数调用
    function fun(msg,time){
        setTimeout(function(){
            console.log(msg);
        },time);
    }
    fun('张三',1000);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    ⑦ 闭包的应用举例

    • (1) 定义模块(具有特定功能的js文件)
      • 将所有的数据和功能封装在一个函数内部(私有的)
      • 只向外部暴露一个包含N个方法的对象和函数
      • 模块的使用者,只需要通风模块暴露的对象调用方法来实现对应的功能
    //闭包应用1-定义模块
    function fun(msg) {
        // 相当于 var msg; 
        function fun1() {
            msg++;
            console.log(msg);
        }
    
        function fun2() {
            msg--;
            console.log(msg);
        }
        return {  // 返回一个对象,把闭包的函数全部返回出来
            fun1: fun1,
            fun2: fun2
        }
    }
    var f = fun(30);
    console.log(f);
    f.fun1();  // 31
    f.fun2();  // 30
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • (2) 点击 li 输出当前li的索引号

    立即执行函数配合闭包的使用很多

    // 闭包应用2-点击li输出当前li的索引号
    // 1. 原本我们可以利用动态添加属性的方式
    var lis = document.querySelector('.nav').querySelectorAll('li');
    for (var i = 0; i < lis.length; i++) {
        lis[i].index = i;
        lis[i].onclick = function() {
            console.log(this.index);
        }
    }
    // 2. 利用闭包的方式得到当前小li的索引号
    for (var i = 0; i < lis.length; i++) {
        // 利用for循环创建了4个立即执行函数
        // 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这个变量
        // 相当于用每一个立即执行函数保存了当前这一次循环的i值,然后传给异步的事件函数使用
        (function(i) {
            // console.log(i);
            lis[i].onclick = function() {
                console.log(i);
            }
        })(i);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • (3) 秒钟之后,打印所有li元素的内容
    // 闭包应用3-3秒钟之后,打印所有li元素的内容
    var lis = document.querySelector('.nav').querySelectorAll('li');
    for (var i = 0; i < lis.length; i++) {
        (function(i) {  // 原理和(2)类似,用每一个立即执行函数保存了当前这一次循环的i值,然后传给异步的定时器使用
            setTimeout(function() {
                console.log(lis[i].innerHTML);
            }, 3000)
        })(i);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • (4) 计算打车价格
    // 闭包应用4-计算打车价格 
    // 打车起步价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)); // 23
    console.log(car.yd(true)); // 33
    
    console.log(car.price(1)); // 13
    console.log(car.yd(false)); // 13
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    思考题

    // 思考题 1:
    var name = "The Window";
    var object = {
        name: "My Object",
        getNameFunc: function() {
            return function() {
                return this.name;
            };
        }
    };
    
    console.log(object.getNameFunc()())
    
    // 思考题 2:
    var name = "The Window";
    var object = {
        name: "My Object",
        getNameFunc: function() {
            var that = this;
            return function() {
                return that.name;  // 闭包
            };
        }
    };
    console.log(object.getNameFunc()())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    答案: (1) “The Window” (2) “My Object”

    七、正则表达式

    1. JS 中正则表达式的书写方式

    // 1. 利用 RegExp对象来创建 正则表达式
    var regexp = new RegExp(/123/);
    
    // 2. 利用字面量创建 正则表达式
    var rg = /123/;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • test 方法用来检测字符串是否符合正则表达式要求的规范(检测一个字符串是否匹配某个模式)

    • 如果匹配则返回 true,否则就返回 false

    var rg = /123/;
    
    console.log(rg.test(123));  // true
    console.log(rg.test('abc'));  // false
    
    • 1
    • 2
    • 3
    • 4

    2. 常用的元字符

    ① 定位符

    字符描述
    ^以…开头。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
    $以…结尾。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
    // 边界符 ^ $ 
    var rg = /abc/; // 正则表达式//里面不需要加引号,不管是数字型还是字符串型
    // /abc/ 只要包含有abc这个字符串返回的都是true
    console.log(rg.test('abc'));  // true
    console.log(rg.test('abcd'));  // true
    console.log(rg.test('aabcd'));  // true
    console.log('---------------------------');
    var reg = /^abc/;
    console.log(reg.test('abc')); // true
    console.log(reg.test('abcd')); // true
    console.log(reg.test('aabcd')); // false
    console.log('---------------------------');
    var reg1 = /^abc$/; // ^$ 精确匹配 要求必须是 abc字符串才符合规范
    console.log(reg1.test('abc')); // true
    console.log(reg1.test('abcd')); // false
    console.log(reg1.test('aabcd')); // false
    console.log(reg1.test('abcabc')); // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    ② 限定符

    字符描述
    *匹配前面的子表达式0次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。***** 等价于 {0,}
    +匹配前面的子表达式1次或多次。例如,zo+ 能匹配 “zo” 以及 "zoo",但不能匹配 “z”+ 等价于 {1,}
    ?匹配前面的子表达式0次或1次。例如,do(es)? 可以匹配 “do”“does”“doxy” 中的 “do”? 等价于 {0,1}
    []匹配[]所包含的字符集合中任意一个字符。例如,"[abc]“可以匹配"plain"中的"a”
    [^]匹配未包含在[]中的一个任意字符([]的取反)。例如,"[ ^abc ]可以匹配"plain"中任何一个字母
    [n1-n2]匹配包含的一个任意字符。例如,"[a-z]"可以匹配"plain"中任何一个字母。
    {n}n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 “Bob” 中的 o,但是能匹配 “food” 中的两个 o
    {n,}n 是一个非负整数。至少匹配 n 次。例如,o{2,} 不能匹配 “Bob” 中的 o,但能匹配 “foooood” 中的所有 oo{1,} 等价于 o+o{0,} 则等价于 o*
    {n,m}m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 “fooooood” 中的前三个 oo{0,1} 等价于 o?。请注意在逗号和两个数之间不能有空格。
    • *、+、?
    // 量词符(限定符): 用来设定某个模式出现的次数
    // 简单理解: 就是让下面的a这个字符重复多少次
    var reg = /^a$/;  // a必须只出现一次
    
    // * 相当于 >= 0 可以出现0次或者很多次(任意个)
    var reg = /^a*$/;
    console.log(reg.test(''));  // true
    console.log(reg.test('a'));  // true
    console.log(reg.test('aaaa'));  // true
    
    // + 相当于 >= 1 可以出现1次或者很多次
    var reg = /^a+$/;
    console.log(reg.test(''));  // false
    console.log(reg.test('a'));  // true
    console.log(reg.test('aaaa'));  // true
    
    // ?  相当于 1 || 0
    var reg = /^a?$/;
    console.log(reg.test(''));  // true
    console.log(reg.test('a'));  // true
    console.log(reg.test('aaaa'));  // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • []、[^]、[n1-n2]
    //var rg = /abc/;  只要包含abc就可以 
    // 字符类: [] 表示有一系列字符可供选择,只要匹配其中一个就可以了
    var rg = /[abc]/; // 只要包含有a 或者 包含有b 或者包含有c 都返回为true
    console.log(rg.test('andy'));  // true
    console.log(rg.test('baby'));  // true
    console.log(rg.test('color'));  // true
    console.log(rg.test('red'));  // false
    
    var rg1 = /^[abc]$/; // 三选一 只有是a 或者是 b  或者是c 这三个字母才返回 true
    console.log(rg1.test('aa'));  // false
    console.log(rg1.test('a'));  // true
    console.log(rg1.test('b'));  // true
    console.log(rg1.test('c'));  // true
    console.log(rg1.test('abc'));  // false
    
    var reg = /^[a-z]$/; // 26个英文字母任何一个字母返回 true  - 表示的是a 到z 的范围  
    console.log(reg.test('a'));  // true
    console.log(reg.test('z'));  // true
    console.log(reg.test(1));  // false
    console.log(reg.test('A'));  // false
    
    // 字符组合
    var reg1 = /^[a-zA-Z0-9_-]$/; // 26个英文字母(大写和小写都可以)任何一个字母返回 true  
    console.log(reg1.test('a'));  // true
    console.log(reg1.test('B'));  // true
    console.log(reg1.test(8));  // true
    console.log(reg1.test('-'));  // true
    console.log(reg1.test('_'));  // true
    console.log(reg1.test('!'));  // false
    
    // 如果中括号里面有^ 表示取反的意思 千万别和 我们边界符 ^ 混淆
    var reg2 = /^[^a-zA-Z0-9_-]$/;
    console.log(reg2.test('a'));  // false
    console.log(reg2.test('B'));  // false
    console.log(reg2.test(8));  // false
    console.log(reg2.test('-'));  // false
    console.log(reg2.test('_'));  // false
    console.log(reg2.test('!'));  //true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • {}、{n,}、{n,m}
    // {3} 就是必须重复3次
    var reg = /^a{3}$/;
    console.log(reg.test(''));  // false
    console.log(reg.test('a'));  // false
    console.log(reg.test('aaaa'));  // false
    console.log(reg.test('aaa'));  // true
    // {3,}  大于等于3
    var reg = /^a{3,}$/;
    console.log(reg.test(''));  // false
    console.log(reg.test('a'));  // false
    console.log(reg.test('aaaa'));  // true
    console.log(reg.test('aaa'));  // true
    //  {3,16}  大于等于3 并且 小于等于16
    var reg = /^a{3,6}$/;
    console.log(reg.test(''));  // false
    console.log(reg.test('a'));  // false
    console.log(reg.test('aaaa'));  // true
    console.log(reg.test('aaa'));  // true
    console.log(reg.test('aaaaaaa'));  // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ④ 预定义类

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

    预定义类符号描述
    \d匹配一个数字字符。等价于 [0-9]。
    \D匹配一个非数字字符。等价于 [^0-9]。
    \w匹配字母、数字、下划线。等价于’[A-Za-z0-9_]'。
    \W匹配非字母、数字、下划线。等价于 ‘[^A-Za-z0-9_]’
    \s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
    \S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
    \f匹配一个换页符。等价于 \x0c 和 \cL。
    \n匹配一个换行符。等价于 \x0a 和 \cJ。
    \r匹配一个回车符。等价于 \x0d 和 \cM。
    \t匹配一个制表符。等价于 \x09 和 \cI。
    \v匹配一个垂直制表符。等价于 \x0b 和 \cK。
    var reg = /^\d+$/;
    console.log(reg.test('1'));  // true
    console.log(reg.test('123'));  // true
    console.log(reg.test('12124353240'));  // true
    
    • 1
    • 2
    • 3
    • 4

    ⑤ 其他

    字符描述
    |使用或者(or)的方式找出多个字符。例如,"^a|b$“可以匹配"a"或"b”。
    ()分组操作,()中视为个整体
    \u4e00-\u9fa5匹配中文
    var reg = /^a|b$/;
    console.log(reg.test('ab'));  // false
    console.log(reg.test('a'));  // true
    console.log(reg.test('b'));  // true
    
    var reg = /^(abc){3}$/; // 它是让abc重复三次
    console.log(reg.test('abc'));  // false
    console.log(reg.test('abcabcabc'));  // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    括号类总结

    // 中括号 字符集合  -  匹配方括号中的任意字符. 
    var reg = /^[abc]$/;
    // a 也可以 b 也可以 c 可以  a ||b || c
    
    // 大括号  量词符 -  里面表示重复次数
    var reg = /^abc{3}$/; // 它只是让c重复三次   abccc
    console.log(reg.test('abc'));  // false
    console.log(reg.test('abcabcabc'));  // false
    console.log(reg.test('abccc'));  // true
    
    // 小括号 -  表示分组,()内视为一个整体,也可以理解为优先级符号
    var reg = /^(abc){3}$/; // 它是让abc重复三次
    console.log(reg.test('abc'));  // false
    console.log(reg.test('abcabcabc'));  // true
    console.log(reg.test('abccc'));  // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    ⑥ 修饰符

    修饰符含义描述
    iignore - 不区分大小写将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别
    gglobal - 全局匹配查找所有的匹配项
    mmulti line - 多行匹配使边界字符 ^$ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾
    s特殊字符圆点 . 中包含换行符 \n默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n
    y (ES6)全局匹配,后者将从上次匹配成功的下一个位置开始。确保匹配必须从剩下的第一个位置开始(粘连)y 修饰符则确保匹配必须从剩下的第一个位置开始,这就是粘连的含义
    u (ES6)“Unicode模式”,用于正确处理大于 \uFFFF 的 Unicode字符4字节的UTF-16编码将被正确地处理
    gi全局匹配 + 不区分大小写查找所有的匹配项,忽略大小写

    注意:这些修饰符可以像"gi"一样同时使用多个

    var str="Google runoob taobao runoob"; 
    var n1=str.match(/runoob/);   // 查找第一次匹配项
    var n2=str.match(/runoob/g);  // 查找所有匹配项
    
    var str="Google runoob taobao RUNoob"; 
    var n1=str.match(/runoob/g);   // 区分大小写
    var n2=str.match(/runoob/gi);  // 不区分大小写
    
    var str="runoobgoogle\ntaobao\nrunoobweibo";
    var n1=str.match(/^runoob/g);   // 匹配一个
    var n2=str.match(/^runoob/gm);  // 多行匹配
    
    var str="google\nrunoob\ntaobao";
    var n1=str.match(/google./);   // 没有使用 s,无法匹配\n
    var n2=str.match(/runoob./s);  // 使用 s,匹配\n
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3. 常用的方法

    // ① search() : 检索与正则表达式相匹配的字符串
    var str = 'hello world!';
    // 一般用法:本质也是把字符串转换为了正则表达式
    var m = str.search('world'); // 6
    console.log(m);
    var n = str.search(/world/i); // 6
    console.log(n);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.1 正则常用的字符串实例方法

    方法描述
    search()使用表达式来搜索匹配,然后返回匹配的位置。
    replace()返回模式被替换处修改后的字符串。
    match()找到一个或多个正则表达式的匹配。
    matchAll()返回一个包含所有匹配正则表达式和分组捕获结果的遍历器
    split()把字符串分割为字符串数组
    ① search()
    • 返回正则匹配到的(符合正则规则的)字符串的位置

    search() 通过正则在字符串中进行查找,如果查询到即范围对应字符串的位置,如果没查询到即范围-1,代码如下:

    let str = 'hello world';
    let pos1 = str.search(/e/);   // 1    
    let pos2 = str.search(/q/);   // -1
    
    • 1
    • 2
    • 3
    ② replace 利用正则表达式替换字符
    • 替换字符串

    请添加图片描述

    var str = 'andy和red';
    // var newStr = str.replace('andy', 'baby');
    var newStr = str.replace(/andy/, 'baby');
    console.log(newStr);  // baby和red
    
    // 案例:屏蔽(替换)敏感词
    var text = document.querySelector('textarea');
    var btn = document.querySelector('button');
    var div = document.querySelector('div');
    btn.onclick = function() {
        div.innerHTML = text.value.replace(/激情|gay/g, '**');  // 所有出现的"激情"和"gay"都替换成"**"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    replace() 的第二个参数,除了可以是要替换成的新字符串,也可以是一个回调函数,通过回调函数可以让替换实现更复杂的需求,上面代码用回调函数改写后的样子,即:

    let str = 'hello world';
    let ret = str.replace(/hello/, function(){    // hi world
        return 'hi';
    });
    
    • 1
    • 2
    • 3
    • 4

    在回调函数的参数中,可以得到相关的一些值,如:正则匹配到的结果就会以第一个参数传递给回调函数

    let str = 'hello world';
    let ret = str.replace(/hello/, function(ret){    // HELLO world
        return ret.toUpperCase();
    });
    
    • 1
    • 2
    • 3
    • 4
    ③ match()
    • 返回正则匹配到的(符合正则规则的)字符串的一些信息

    match() 可以把正则匹配到的结果,返回一个数组,如果没有匹配成功的话,将返回 null

    let str = 'hello world';
    let arr1 = str.match(/e/);   // ['e', index: 1, input: 'hello world', groups: undefined]
    let arr2 = str.match(/q/);   // null
    
    • 1
    • 2
    • 3

    除了可以匹配到值以外,还能匹配到位置等一些其他信息。

    let str = 'hello world';
    let arr = str.match(/e/);   // ["e", index: 1, input: "hello world", groups: undefined]
    
    • 1
    • 2

    但是match有一个问题,就是在全局匹配模式下,只能得到匹配的值,但是得不到其他相关信息。

    let str = 'hello world';
    let arr = str.match(/e/g);   // ["e"] 只有所有字符串中e的值
    
    • 1
    • 2
    ④ matchAll()
    • 返回一个包含所有匹配正则表达式和分组捕获结果的遍历器

    matchAll() 就是为了解决上面match() 中遇到的问题,当全局匹配的时候,也能得到详细的信息,不过matchAll()返回的并不是一个数组,而是返回一个遍历器,即Iterator。利用JavaScript的扩展运算符可以非常方便的把遍历器对象转换成数组对象。

    let str = 'hello world';
    let arr = [...str.matchAll(/l/g)];   
    /* 
    	[
            ["l", index: 2, input: "hello world", groups: undefined],
            ["l", index: 3, input: "hello world", groups: undefined],
            ["l", index: 9, input: "hello world", groups: undefined]
        ] 
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    因为返回的是遍历器,所以通常使用 for...of 循环 取出

    ④ split()
    • 分割字符串

    split() 为字符串分割成数组,可以通过一个分隔符进行分割,即:

    let str = '1+2+3+4';
    let arr = str.split('+');    // ["1", "2", "3", "4"]
    
    • 1
    • 2

    split() 在正则操作中,可以以正则作为分隔符进行操作,即:

    let str = 'a1b2c3d';
    let arr = str.split(/\d/);    // ["a", "b", "c", "d"]  // 以各种数字为分隔符
    
    • 1
    • 2

    3.2 正则常用的正则实例方法(RegExp 对象方法)

    方法描述
    test()判断正则模式是否在字符串中出现过,然后根据结果返回 true 或 false
    exec()检索字符串中指定的值。返回找到的值,并确定其位置。如果未找到匹配,则返回 null
    compile()返回正则表达式的字符串
    toString()在 1.5 版本中已废弃。 编译正则表达式
    ① test()
    • 判断是否存在

    test() 判断正则是否在字符串中出现过,如果出现返回 true,如果没出现返回false

    let str = 'hello world';
    let ret1 = /e/.test(str);   // true  
    let ret2 = /q/.test(str);   // false
    
    • 1
    • 2
    • 3
    ② exec()
    • 返回正则匹配到的(符合正则规则的)字符串的一些信息

    exec() 跟 match() 类似,也是返回匹配到的数组,如果没有匹配成功也是返回null

    let str = 'hello world';
    let arr1 = /e/.exec(str)   // ['e', index: 1, input: 'hello world', groups: undefined]
    let arr2 = /q/.exec(str);   // null
    
    • 1
    • 2
    • 3

    区别在于exec() 在全局模式下,可以多次调用返回不同的值信息,如下:

    let str = 'hello world';
    let re = /l/g;
    let arr1 = re.exec(str);   // ["l", index: 2, input: "hello world", groups: undefined]
    let arr2 = re.exec(str);   // ["l", index: 3, input: "hello world", groups: undefined]
    let arr3 = re.exec(str);   // ["l", index: 9, input: "hello world", groups: undefined]
    let arr4 = re.exec(str);   // null
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当匹配不到结果的时候,才会返回null,所以在有g的情况下使用的时候要额外的小心,其实test() 也是具备这个特性的,例如:

    let str = 'hello world';
    let re = /e/g;
    let ret1 = re.test(str);   // true
    let ret2 = re.test(str);   // false
    let ret3 = re.test(str);   // true
    let ret4 = re.test(str);   // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    ③ compile()
    • 在脚本执行过程中编译正则表达式
    • 改变和重新编译正则表达式
    let str = "hello world";
    let patt = /hello/g;
    let str2 = str.replace(patt, "Hi");
    document.write(str2);  // Hi world
    
    patt = /world/g;
    patt.compile(patt); // 改变、重新编译正则表达式
    str2 = str.replace(patt, "你好");
    document.write(str2);  // hello 你好
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    我眼中的大数据(一)
    python递归算法
    C++速通LeetCode简单第9题-二叉树的最大深度
    exness整理马斯克收购推特需要解决三个问题
    前端埋点-分析用户在线时间
    (leetcode)单值二叉树
    Kubernetes入门
    接口测试面试秘籍,一套搞定接口测试
    一文浅谈Mockito使用
    基于SSH开发新闻管理系统
  • 原文地址:https://blog.csdn.net/Syc1102g/article/details/126331259