likes
comments
collection
share

this详解,手写call、apply、bind

作者站长头像
站长
· 阅读数 13

this不管是在我们的日常开发中还是在我们的面试中,用的都比较多。可以说对一个前端来讲还是非常重要的。 下面我们就从以下两个方面来说一下this的指向问题:

  • 非严格模式
  • 严格模式

this概念

this表示当前对象的一个引用,在js中this并不是固定不变的,它会随着执行环境的改变而改变,简单来说this永远指向最后调用它的哪个对象

非严格模式下的this指向

调用绑定

//函数直接调用和匿名函数的调用this指向window
console.log(this)//window


function foo(){
    console.log(this)
}
foo()//window


(function (){
   console.log(this) //window
})()


//嵌套函数调用
let obj={
    a:function(){
        console.log(this)
    }
}
obj.a()//obj

隐式绑定

//隐式丢失

//方法一
function foo(){
    console.log(this)
}
let obj={
    foo:foo
}
let bar=obj.foo
obj.foo()//obj
bar()//window


//方法二
function foo(fn){
    fn()
}
function bar(){
    console.log(this)
}
let obj={
    bar:bar
}
foo(obj.bar)//window


//间接改变
function foo(){
    console.log(this)
}
let obj1={
    foo:foo
}
let obj2={
    
}
obj2.foo=obj1.foo
obj2.foo()//obj2

new 关键字绑定

let that=null
function Foo(){
    that=this
    console.log(this)
}
let foo=new Foo()
console.log(that===foo)

显示绑定

//使用call函数、apply函数和bind函数
let obj1={
    foo:function(){
        console.log(this)
    }
}
let obj2={
    bar:function(){
        console.log(this)
    }
}
obj1.foo.call(obj2)//obj2
obj1.foo.apply(obj2)//obj2
obj1.foo.bind(obj2)()//obj2

call、apply、bind三种方法的区别

三个方法都是改变this的一个方法,但是三种方法又略有不同,首先call方法和apply方法的区别是传参方面不同 call接受多个参数,例如

call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)

apply接受两个参数,第一个参数是this值,第二个参数是一个带下标的集合,这个集合可以是数组,也可以是类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数 bind方法是返回一个绑定好this的函数,之后无论如何调用都不会改变,其传参与call一样

手写call、apply、bind

/* 
call 第一个参数是this(在非严格模式下,null和undefined将会被替换为全局对象)
arg1, arg2,…, argN 之后的多个参数都会传入函数中
*/
function myCall(context, ...arg) {
  // 判断是否函数,避免其他数据类型调用call,this表示的是当前执行的函数
  if (typeof this !== "function") {
    console.log("类型错误");
    return;
  }
  let res = null;
  // 判断传入的context是否是null或undefined
  context = context || window;
  // 这里fn可能会与context重名
  context.fn = this;
  // 调用函数
  res = context.fn(...arg);
  // 删除添加的属性
  delete context.fn;
  // 返回结果
  return res;
}
Function.prototype.myCall = myCall;

function Car(price, color) {
  console.log(this.name, price, color);
}
Car.myCall({ name: "123" }, 111, "red");
/* 
apply 接受两个参数,第一个参数是this,在非严格模式下,则null和undefined会被
替换为window,原始值会被转换为对象

第二个参数是可选,一个类数组对象,用于指定调用func时的参数,或者如果不需要向函数提供参数则为null或undefined
*/
function myApply(context, arr) {
  // 判断调用对象是否为函数,避免原始值调用
  if (typeof this !== "function") {
    console.log("类型错误");
    return;
  }
  // 函数执行结果
  let res = null;
  // 判断context是否为null或undefined,如果是则赋值为window
  context = context || window;
  context.fn = this;
  // 调用函数(arr可能传入undefined或null)
  if (arr) {
    res = context.fn(...arr);
  } else {
    res = context.fn();
  }
  delete context.fn;
  return res;
}
Function.prototype.myApply = myApply;
function Car(price, color) {
  console.log(this.name, price, color);
}
Car.myApply({ name: "奔驰" }, [111, "red"]);
/* 
第一个参数是绑定的this,如果在非严格模式下,null和undefined会被替换为window,
并且原始值会被转换为对象。如果使用new运算符构造绑定函数,则忽略该值

之后的参数与call类似
*/
function myBind(context, ...arg) {
  context = context || window;
  let self = this;
  let fn = function () {};
  let _fn = function () {
    // 如果是new操作,则忽略传入this绑定的值
    return self.apply(this instanceof _fn ? this : context, arg);
  };
  fn.prototype = this.prototype;
  _fn.prototype = new fn();
  return _fn;
}

Function.prototype.myBind = function () {
  let outContext = arguments[0]; // 取上下文
  let outArgs = Array.from(arguments).slice(1); // 取外部入参
  const outThis = this; // 存外部this
  let cb = function () {
    const isNew = typeof new.target !== "undefined"; // 判断函数是否被new过
    const inArgs = Array.from(arguments); // 取内部入参
    return outThis.apply(
      isNew ? this : outContext,
      outArgs.concat(inArgs)
    ); // 改变指向,合并函数入参
  };
  cb.prototype = outThis.prototype; // 继承构造函数原型
  return cb; // 返回创建函数
};

// 试一下
var obj = { name: 1, age: 2 };
var name = "Leo",
  age = 18;
function Fn(height, Gender) {
  console.log(
    "name:",
    this.name,
    "age:",
    this.age,
    "height:",
    height,
    "Gender:",
    Gender
  );
}
Fn.prototype.say = function () {
  console.log("Fn.prototype.say");
};

var fn1 = Fn.myBind_3(obj, "80cm");
var obj1 = new fn1("male"); // name: undefined age: undefined height: 80cm Gender: male
obj1.say(); // Fn.prototype.say

var fn1 = Fn.bind(obj, "80cm");
var obj1 = new fn1("male"); // name: undefined age: undefined height: 80cm Gender: male
obj1.say(); // Fn.prototype.say

箭头函数

箭头函数与普通函数的区别

箭头函数没有自己的this、arguments、super或new.target,并且它不能作为构造函数 箭头函数的this是通过继承来的(根据它书写的位置的函数的执行上下文来确定)(有一个简单的小口诀就是它的this是它上一层的爸爸)

箭头函数的this的指向

//最外层的箭头函数的this
let foo=()=>{
  console.log(this)
}
foo()//window(这里的this指向并不能改变)



//函数里边的箭头函数
function foo(){
  let bar=()=>{
    console.log(this)//这里的this继承的是foo的this指向(this:箭头函数的上一层的爸爸)
    /* 
    箭头函数是bar这一层,上一层是foo,foo的爸爸是window
    */
  }
  bar()
}
let obj={}
foo()//window
foo.call(obj)
/* obj(通过改变foo的this进而间接改变了里边箭头函数的this,
而箭头函数的this还是那个位置的值,这也就是我最开始的时候说改变加引号的原因) */

严格模式下的this指向

严格模式下的this指向和普通模式下差不多,不同的是全局函数的this,不能指向window而是undefined

全局作用域中的this

在严格模式下,全局作用域中this指向window对象

"use strict"
console.log(this);// window

全局作用域中的this

无显示指定

"use strict"
function foo() {
  console.log(this);// undefined
}
foo()

显示指定

"use strict"
function foo() {
  console.log(this);// window
}
foo.call(this)

练习题

var obj = {
	a:1,
	c:2,
	say:function(a){
         console.log("this1: "+this)
		var sayA = function(a){
			console.log("this2: "+this)
			this.a = a
		}
        function sayC(){
             console.log("this3: "+this)
         }
		sayA(a)
		sayC()
	}
}
obj.say(3)
/* 
this1:obj
this2:window
this3:window
*/
console.log(obj.a+"  "+obj.c)//1 2
console.log(window.a+"  "+window.c)//3 undefined
function Person(name,age){
      this.name = name
      this.age = age
}
var person1 = new Person("张三",18)
var person2 = Person("李四",12)
console.log(person1)//{name:张三,age:18}
console.log(person2)//undefined
console.log(person1.name,person1.age)//张三,18
console.log(window.name,window.age)//李四,12
//练习题三答案
function Test(a, b) {
    this.a = a
    this.b = b
    this.say = function (a, b) {
      	this.a = a
       	this.b = b
       	console.log(this)
        console.log(a, b)
     }
}
var obj1 = new Test(1, 1)
console.log(obj1.a, obj1.b)//1,1
obj1.say()//obj1,undefined,undefined
console.log(obj1.a, obj1.b)//undefined,undefined
var obj2 = { a: 2, b: 2 }
obj1.say(4, 4)//obj1,4,4
console.log(obj1.a, obj1.b)//4,4
obj1.say.call(obj2, 3, 3)//obj2,3,3
console.log(obj2.a + "--" + obj2.b)//3--3
//练习题四答案
let obj = {
    func() {
        const arrowFunc = () => {
            console.log(this)
        } 
        return arrowFunc
    },
    _name: "obj",
}
//箭头函数的this指向一直是上一层的爸爸(即func的this指向)
obj.func()()//obj
//func是一个全局变量它的this指向的是window
func = obj.func
func()()//window
obj.func.bind({ _name: "newObj" })()()//{ _name: "newObj" }
obj.func.bind()()()//window
obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })()//{ _name: "bindObj" }
//bind会返回绑定好this的函数供调用
//解释bind改变的是func的this指向,之后无论如何调用都不会改变
//练习题五答案
var length = 10
function fn () {
  console.log(this.length)
}

var obj = {
  length: 5,
  method: function (fn) {
    fn()//10
    arguments[0]()//2
    //相当于arguments调用arguments取出第一个值fn(类似obj.属性调用,对象也可以根据[]的方式取出)
  }
}
obj.method(fn, 1)