剖析this\apply\call\bind

209 阅读5分钟

一、 call的模拟实现

func.call(thisArg, arg1, arg2, ...)        // call 用法 

实现的原理:call() 改变this指向 并且执行func函数

所以模拟的步骤分三步完成:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数

Function.prototype.callOwn = function(thisArg, args) {
    // this指向调用callOwn 的对象    if (typeof this !== 'function') { // 调用callOwn 的若不是函数则报错        throw new TypeError('Error')
    }
    thisArg = thisArg || window // null时候执行window
    thisArg.fn = this   // 将调用callOwn 函数的对象添加到thisArg的属性中    const result = thisArg.fn(...[...arguments].slice(1)) // 执行该属性,
    delete thisArg.fn   // 删除该属性
    return result
}
thisArg.fn(...[...arguments].slice(1)) 这是对不定参数的处理,取出第二个到最后一个参数,进行函数调用赋值

callOwn 案例走起

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.callOwn (foo, 'kevin', 18);

 二、apply的模拟实现

func.apply(thisArg, [arg1, arg2, ...])     // apply 用法 

Function.prototype.apply = function(thisArg, args) {
    if (typeof this !== 'function') { 
        throw new TypeError('Error')
    }
    thisArg = thisArg || window
    thisArg.fn = this
    let result
    if(args) {
        result = thisArg.fn(...args)
    } else {
        result = thisArg.fn()
    }
    delete thisArg.fn
    return result
}

模拟实现的call和apply只是模拟了引用类型和null情况,针对基本类型没有做一个处理。

call\apply参考链接

call和apply的注意点 这两个方法在调用的时候,如果我们传入数字或者字符串,这两个方法会把传入的参数转成对象类型。

var foo = 'string', number = 1

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this);
}

bar.call (foo, 'kevin', 18);
bar.apply(number,['kevin', 18])

三、new的模拟实现

  1. 创建一个新对象
  2. 将新对象的proto指向原对象的 prototype
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回这个新对象

function new() {
    // 创建一个新的对象
    const obj = {}    
   // 获取第一个参数,arguments是类数组,不可直接调用shift方法
    //此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
    const Constructor = [].shift.call(arguments)  
   // 将obj的原型指向构造函数的原型对象,这样obj就可以访问构造函数原型上的属性
    obj.__proto__ = Constructor.prototype
    // 将构造函数的this指向obj,这样obj就可以访问到构造函数中的属性   
    const res = Constructor.apply(obj, arguments);
    return typeof res === 'object' ? res : obj;
}

案列走起

function New() {
    const obj = {}    
    const Constructor = [].shift.call(arguments) 
    obj.__proto__ = Constructor.prototype // 构造函数的prototype 
    const res = Constructor.apply(obj, arguments);
    return typeof res === 'object' ? res : obj;
}
function A (name,age) {
   this.name = name
   this.age = age
}
var obj = New(A, 1, 2); 等价 var obj = new A(1, 2);
console.log(obj)

四、bind的模拟实现

func.bind(thisArg[, arg1[, arg2[, ...]]])    // bind 用法

Function.prototype.bind = function(thisArg) {
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }
    // 存储函数本身
    const _this  = this;
    // 去除thisArg的其他参数 转成数组
    const args = [...arguments].slice(1)
    // 返回一个函数
    const bound = function() {
        // 可能返回了一个构造函数,我们可以 new F(),所以需要判断
        if (this instanceof bound) {
            return new _this(...args, ...arguments)
        }
        // apply修改this指向,把两个函数的参数合并传给thisArg函数,并执行thisArg函数,返回执行结果
        return _this.apply(thisArg, args.concat(...arguments))
    }
    return bound
}

bind 方法 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

案列走起

Function.prototype.bind1 = function(thisArg) {
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }
    const _this  = this;
    const args = [...arguments].slice(1)
    const bound = function() {
        if (this instanceof bound) {
            return new _this(...args, ...arguments)
        }
        return _this.apply(thisArg, args.concat(...arguments))
    }
    return bound
}
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
var bind2 = bar.bind1(foo,'name','age')
console.log(bind2())

 五、this 指向的技巧判定

非严格模式下 this 指向全局对象(浏览器下指向 Window,Node.js 环境是 Global ), 严格模式下,this 绑定到 undefined , 严格模式不允许this指向全局对象。 

bar() => bar.call(undefined) 严格模式下bar函数的this指向undefined 
bar() => window.bar.call(window) 非严格模式下bar函数的this指向window 
obj.foo() => obj.foo.call(obj) 其中foo函数的ths指向obj 

由例子分解开始:注意普通函数和箭头函数的指向区别

var name = 'window';
 var person = {
   name: 'person', 
   sayHi: function(){
    console.log('sayHi', this.name)
   },
   sayTime: function() {
	console.log('sayTime', this.name)
     setTimeout(function(){ // this指向调用函数的对象
      console.log('setTimeout', this.name)
    }, 100)
  },
 arrow: function() {
   setTimeout(() => { // this指向最近作用域
      console.log('arrow', this.name)
    }, 100)
 }
 } 
 person.sayHi(); // person.sayHi.call(person)
 person.sayTime() // person.sayTime.call(person)
 person.arrow() //  person.arrow.call(person)

 解剖里面的这里this指向

person.sayHi(); // person.sayHi.call(person) 所以sayHi函数里面的this指向person
console.log('sayHi', this.name) // 输出person

person.sayTime() // person.sayTime.call(person) 所以sayHi函数里面的this指向person
console.log('sayTime', this.name) // 输出person

setTimeout 和 setInterval 调用函数是 window.fn() => 所以this指向window
console.log('setTimeout', this.name) // 输出 window

person.arrow() //  person.arrow.call(person) 所以arrow函数里面的this指向person
console.log('arrow', this.name) 
// 同样是window.fn() => 所以this指向window 但是但是但是注意了这是箭头函数所以这里箭头函数this指向最近层作用域arrow,arrow函数里面的this指向person,所以
console.log('arrow', this.name) // 输出person

 []语法中的this

function fn() {
  console.log(this.length)
}  
var arr = [1, 2, fn]
arr[2]()   
// 可以看做是 arr.2.call(arr) ==> fn.call(arr) ==> fn函数this指向arr
// 输出 3 

或者此函数

var length = 10;
function fn() {
    console.log(this.length);
}
var obj = {
  length: 5,
  method: function(fn) { 
    console.log(this) 
    console.log(arguments)
    fn(); // window.fn() 
    arguments[0](); // fn.call([fn,1])
  }
};
obj.method(fn, 1)

  arguments[0](); ==> arguments.0.call(arguments) ==> fn.call([fn,1])

this参考链接

箭头函数this和普通函数

箭头函数没有自己的 this,当在内部使用了 this时,它会指向最近一层作用域(函数作用域和全局作用域)内的 this。由于没有自身的this,所以只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域,并指向调用该普通函数的对象。

var name = 'hello'
var obj = {
    name: 'latency',
    arrow: function(){
	// console.log(this) // 最近的作用域
        return () => { // 箭头函数内部没有this,指向最近的作用域,会逐级向上查找
            console.log('name:', this.name);
        }
    },
    basic: function(){
	// console.log(this) 
        return function () { // 自身有this,指向调用闭包的对象
            console.log('name:', this.name);
        }
    }
}
obj.arrow()()  // 输出latency
// 可以 看做是 var fn = obj.sayName()  window.fn() // window.fn() 虽然是window调用了闭包,但是箭头函数this查找机制是按照自己来的,这是和普通的函数的区别
obj.basic()()  // 输出hello

箭头函数和普通函数的this指向区别

永远记住这两点OK
箭头函数: this 它会指向最近一层作用域
普通函数:this 永远指向调用它的对象

总归:

我们在使用函数的调用,初衷当然都是想让this指向调用的当前函数的对象,所以什么时候用箭头函数也要留神。比如。

var name = 'cheng';
var obj = {
    name: 'latency',
    sayName: () => {
        console.log('name:', this.name) 
    }
}
obj.sayName()  // sayName整体是箭头函数,所有this就指向了window。

所以在函数中需要使用闭包的时候,用箭头函数就很方便了。

经典题目

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () { 
    return function () { 
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2() 
person1.show2.call(person2) 

person1.show3()() 
person1.show3().call(person2) 
person1.show3.call(person2)() 

person1.show4()()
person1.show4().call(person2) 
person1.show4.call(person2)()

总算弄清楚了。很开森。。。