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

构造函数

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与 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)
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容