美文网首页
构造函数、this 指向、函数防抖和节流

构造函数、this 指向、函数防抖和节流

作者: 欢欣的膜笛 | 来源:发表于2021-05-17 13:02 被阅读0次

    构造函数

    构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与 new 运算符一起使用在创建对象的语句中。没有返回值,不能被直接调用,必须通过 new 运算符在创建对象时才会自动调用。

    class

    ES6 引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。Class 不存在变量提升(hoist)。
    constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。
    constructor 方法默认返回实例对象(即 this),类的方法内部如果含有 this,它默认指向类的实例。

    如果一个子类通过extends关键字继承了父类,那么在子类的constructor构造函数中必须优先调用一下super

    super 作为函数调用时,代表父类的构造函数。子类必须在 constructor 方法中调用 super 方法,用来新建父类的 this 对象。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super() 在这里相当于A.prototype.constructor.call(this)
    super 作为对象时,指向父类的原型对象。定义在父类实例上的方法或属性,是无法通过 super 调用的。super.print() 实际上执行的是 super.print.call(this)
    使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。console.log(super); // 报错

    ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。

    大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。
    (1)作为一个对象,子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
    (2)作为一个构造函数,子类的原型 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的实例 prototype 属性。

    public:允许在类的内外被调用
    private:只允许在类的内部被调用
    protected:允许在类的内部及继承的子类中调用
    static:静态方法/属性。需要直接通过类名来调用,不能通过 new 操作符生成的实例对象进行调用。故转换成 es5 写法时,应直接绑定在构造函数上,而不是绑定在 prototype 上。Dog.sayName = function() { console.log('123') }
    抽象类: abstract 修饰, 里面可以没有抽象方法。但有抽象方法(abstract method)的类必须声明为抽象类(abstract class),抽象方法 ,可以不包含具体实现,但是要求子类中必须实现此方法。

    实现一个 new

    function _new(fn, ...arg) {
          // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
          const obj = Object.create(fn.prototype);
          const ret = fn.apply(obj, arg);
          return ret instanceof Object ? ret : obj;
    }
    // 实现一个new
    var Dog = function(name) {
          this.name = name
    }
    Dog.prototype.sayName = function() {
          console.log('my name is ' + this.name)
    }
    let sanmao = new Dog('三毛')
    let simao = _new(Dog, '四毛')
    sanmao.sayName()
    simao.sayName()
    

    new 做了什么?

    1. 创建一个空对象 objvar obj = {}
    2. obj__proto__成员指向了 Base 函数对象 prototype 成员对象:obj.__proto__ = Base.prototype
    3. Base 函数对象的 this 指针替换成 obj,再调用 Base 函数:Base.call(obj)

    原型链(构造函数创建对象的完整的原型链7个框,12条线)

    function Foo() {
        Foo.a = function () {
            console.log(1)
        }
        this.a = function () {
            console.log(2)
        }
    }
    Foo.prototype.a = function () {
        console.log(3)
    }
    Foo.a = function () {
        console.log(4)
    }
    Foo.a(); // 4
    let obj = new Foo();
    obj.a(); // 2:属性上存在,则不会去原型链上查找
    Foo.a(); // 1:Foo.a 被重新赋值了
    
    原型链.png
    • 已知一个构造函数,该构造函数的实例,可以通过 __proto__ 属性访问到原型中的属性和方法,后来发现原型之上还有原型,依次类推,包括数组、正则、函数等等所有的对象类型都可以通过查找一层层的 __proto__ 属性都最终找到了某个对象(原型链的顶端),我们把这个查找过程称之为这个对象的原型链。

    • 凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。普通对象没有 prototype,但有 __proto__ 属性。

    • 对象有属性 __proto__,指向该对象的构造函数的原型对象。

    • 万物皆对象。方法除了有属性 __proto__,还有属性 prototypeprototype 指向该方法的原型对象。

    • 对象的原型链

      1. 结论1:
        • 对象字面量是 Object 构造函数的实例
        • 数组是 Array 构造函数的实例
        • 正则表达式是 RegExp 构造函数的实例
      2. 结论2:
        • Object.prototype.__proto__===null(原型链最顶端)
        • 自定义构造函数的默认的原型对象的 __proto__ 指向 Object.prototype
        • Array、RegExp、String、Number、Boolean 这些函数的原型对象的 __proto__ 都指向 Object.prototype
    • 函数的原型链

      1. 结论1:所有的函数都是 Function 的实例
        • 推论1:函数.__proto__===Function.prototype
        • 推论2:Array.__proto__===Function.prototype
        • 推论3:RegExp.__proto__===Function.prototype
        • 推论4:Object.__proto__===Function.prototype
        • 推论5:Function.__proto__===Function.prototype
      2. 结论2:Function.prototype.__proto__===Object.prototype(原型链最顶端)
    • 区分对象类型

      console.log(Object.prototype.toString.call(123))  // [object Number]
      console.log(Object.prototype.toString.call('123'))  // [object String]
      console.log(Object.prototype.toString.call(undefined))  // [object Undefined]     
      console.log(Object.prototype.toString.call(true))  // [object Boolean]
      console.log(Object.prototype.toString.call({}))  // [object Object]
      console.log(Object.prototype.toString.call([]))  // [object Array]
      console.log(Object.prototype.toString.call(function(){}))  // [object Function]
      

    五种继承

    1. 扩展继承

      function Fn() { }
      // 给构造函数的 prototype 对象添加属性和方法,从而使该构造函数的每个实例都能访问到
      Fn.prototype.say = function () { }
      
    2. 替换继承

      function Fn() { }
      // 重新设置了构造函数的 prototype 的值,让它指向一个全新的对象,从此以后的实例都只能访问到这个对象中的属性和方法
      Fn.prototype = {
          constructor: Fn,
          say: function() { },
          run: function() { }
      }
      
    3. 混入继承(拷贝继承)
      实现功能:将一个对象中的属性和方法分别遍历添加到另一个对象中
      实现方式:for...in... 循环对象的每一个属性

      function Person(obj) {
          // 将 obj 中的属性遍历添加到 this 中
          for(var key in obj) {
              // key 是一个变量,该变量保存了每一次遍历 obj 获取到的属性的名称
              this[key] = obj[key];
          }
      }
      var p3=new Person({
          name: "李四",
          age: 18,
          gender: "未知",
          grade: "小五",
          className: "5(3)班"
      });
      
    4. 原型式继承(经典继承)
      a. 帮助用户创建一个新对象,让这个新对象可以访问到指定对象中的属性和方法
      b. 新对象.__proto__ === 指定对象
      c. 原型式继承不需要关心构造函数,如果你要关心构造函数,那么就不要使用原型式继承
      d. 替换继承是重新创建对象替换掉原来的原型,原型式继承是对象已经存在,将此对象赋给新对象,新对象的原型为原来存在的对象

      var obj = { a:10, b:20, c:30 };
      
      // ES5 中实现了经典继承:Object.create()
      var o1 = Object.create(obj);
      console.log(o1.__proto__ === obj); // true
      console.log(o1.a); // 10
      o1.a = 100;
      console.log(o1.a); // 100,给 o1 自己添加了属性,再也访问不到 obj 中的 a 属性
      
    5. 构造函数继承
      a. 一个称为父类构造函数,一个称为子类构造函数
      b. 如果父类构造函数中的代码完全适用于子类构造函数,就在子类构造函数中运用上下文模式借用父类构造函数,从而给子类的实例添加属性和方法

      function Animal(type) {
          this.type = type
      }
      Animal.prototype.run = function() {
          console.log('run')
      }
      
      function Cat(name, type) {
          // 此处采用上下文调用,使 this 指向 Cat 的实例
          // 故 Cat 的实例可以继承 Animal 的实例上的属性和方法
          Animal.call(this, type)
          this.name = name
      }
      // 让 Cat 继承 Animal 原型链上的方法
      Cat.__proto__ = Animal.prototype
      Cat.prototype.say = function() {
          console.log('say')
      }
      
      console.log(new Cat('abc', 'cat'))
      

    this 指向分析

    1. 函数调用:this 指向 window,返回值由 return 语句决定。若指定了严格模式('use strict'),this 指向 undefined。
    function f1() {};
    f1();
    
    1. 方法调用:this 指向调用者,返回值由 return 语句决定
    const obj = {
        name: "张三",
        say: function() {}
    };
    obj.say();
    
    const obj1 = {
        num: 10,
        hello: function() {
            console.log(this); // obj1
            setTimeout(function() {
                console.log(this); // window,匿名函数没有直接调用者,this 指向 window
            });
            setTimeout(() => {
                console.log(this); // obj1,箭头函数,this 指向最近的函数的 this 指向
            });
        }
    }
    obj1.hello();
    
    1. 构造函数调用
    function Fn(name) {
        this.name = name;
    }
    var f1 = new Fn('zhangsan');
    

    首先 new 关键字会创建一个空的对象,然后会自动调用一个函数 apply 方法,将 this 指向这个空对象,这样的话,函数内部的 this 就会被这个空的对象替代。

    • 若无 return 语句,则默认返回 this 即构造函数的实例。
    • 若有 return 语句,如果 return 了一个基本数据类型,则最终返回 this
    • 若有 return 语句,如果 return 了一个对象,则最终返回这个对象
    1. 上下文调用
    foo.call();
    foo.apply();
    
    • this 由第一个参数决定,返回值由 return 语句决定
    • 第一种情况:实参为 null 或 undefined,函数内部的 this 指向 window
    • 第二种情况:实参为 Number、String、Boolean,函数内部的 this 指向对应的基本包装类型的对象
    • 第三种情况:实参为对象数据,函数内部的 this 指向该对象
    • call 和 apply 的不同:call 方法的第一个实参表示 this 的指向,后面依次表示 foo 函数传递的参数,以逗号隔开;apply 方法的第一个实参表示 this 的指向,第二个参数为数组,表示 foo 函数传递的实参。
    1. bind()
      bind 也可以有多个参数,参数还可以在执行的时候再次添加。但是要注意的是,参数是按照形参的顺序进行的。bind 方法返回的是一个修改过后的函数,需要调用。
      call 和 apply 都是改变上下文中的 this 并立即执行这个函数,bind 方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加。
    var a = {
        user: "追梦子",
        fn: function(e, d, f) {
            console.log(this.user); // 追梦子
            console.log(e, d, f); // 10 1 2
        }
    }
    var b = a.fn;
    var c = b.bind(a, 10);
    c(1, 2);
    
    1. 箭头函数:this 指向上下文函数 this 的指向
    const obj = {
        radius: 10,
        diameter() {
            return this.radius * 2 // 普通函数,this 指向直接调用它的对象 obj
        },
        perimeter: () => 2 * Math.PI * this.radius // 箭头函数,this 指向上下文函数 this 的指向,这里上下文没有函数对象,就默认为 window
    }
    console.log(obj.diameter()) // 20
    console.log(obj.perimeter()) // NaN
    

    箭头函数和普通函数区别

    1. 箭头函数是匿名函数
    2. 箭头函数不能绑定 arguments,取而代之用 rest 参数 ... 解决
    3. 箭头函数没有原型属性,故不能作为构造函数,不能使用 new
    4. 箭头函数的 this 永远指向其上下文函数的 this,没有办改变其指向,普通函数的 this 指向调用它的对象

    函数防抖(debounce)

    防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次。如果设定的时间到来之前,又一次触发了事件,就重新开始延时。


    debounce
    1. 思路:返回一个函数,每次触发事件时都取消之前的定时器

    2. 需要注意问题:this 指向、参数的传递、是否要立即调用一次

    3. 使用场景

      • 监听 resize 或 scroll,执行一些业务处理逻辑
      • 搜索输入框,在输入后 200 毫秒触发搜索
    4. 实现

    function debounce(fn, wait, immediate) {
        let timer = null;
        // 返回一个函数
        return function(...args) { // 匿名函数没有直接调用者,this 指向 window
            // 每次触发事件时都取消之前的定时器
            // timer 是分配一个随机数字 id,clearTimeout 后,timer 的变量指向数字 id 还在, 只是定时器停止了
            clearTimeout(timer);
            // 判断是否要立即执行一次
            if(immediate && !timer) {
                fn.apply(this, args);
            }
            // setTimeout 中使用箭头函数,就是让 this 指向上下文函数 this 的指向
            timer = setTimeout(() => {
                fn.apply(this, args)
            }, wait)
        }
    }
    

    函数节流(throttle)

    函数节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。


    throttle

    有两种思路实现:使用时间戳和定时器

    场景:商品预览图的放大镜效果时,不必每次鼠标移动都计算位置。

    1. 使用时间戳
    function throttle1(fn, wait)  {
        // 记录上一次执行的时间戳
        let previous = 0;
        return function(...args) {
            // 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔,就执行函数,否则不执行
            if(Date.now() - previous > wait) {
                // 更新上一次的时间戳为当前时间戳
                previous = Date.now();
                fn.apply(this, args);
            }
        }
    }
    

    第一次事件肯定触发,最后一次不会触发(比如说监听 onmousemove,则鼠标停止移动时,立即停止触发事件)

    1. 使用定时器
    function throttle(fn, wait)  {
        // 设置一个定时器
        let timer = null;
        return function(...args) {
            // 判断如果定时器不存在就执行,存在则不执行
            if(!timer) {
                // 到达函数执行时间后,设置下一个定时器。确保函数有序触发
                timer = setTimeout(() => {
                    // 把 timer 赋值为 null,是为了释放内存,方便 boolean 判断。定时器并未停止
                    timer = null;
                    // 执行函数
                    fn.apply(this, args)
                }, wait)
            }
        }
    }
    // 第一次事件不会触发(fn是放在 setTimeout中执行的,所以第一次触发事件至少等待 wait 毫秒之后才执行),最后一次一定触发
    
    1. 定时器和时间戳结合
    function throttle(fn, wait)  {
        // 记录上一次执行的时间戳
        let previous = 0;
        // 设置一个定时器
        let timer = null;
        return function(...args) {
            // 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔
            if(Date.now() - previous > wait) {
                clearTimeout(timer);
                timer = null
                // 更新上一次的时间戳为当前时间戳
                previous = Date.now();
                fn.apply(this, args);
            } else if(!timer) {
                // 设置下一个定时器
                timer = setTimeout(() => {
                    timer = null;
                    fn.apply(this, args)
                }, wait)
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:构造函数、this 指向、函数防抖和节流

          本文链接:https://www.haomeiwen.com/subject/gwcrjltx.html