this以及bind、apply与call

call、apply、bind的作用是改变函数运行时this的指向。

一、this

1、普通函数中的this

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

var a = 1;
function fn(){
    console.log(this.a);
}
fn();  //输出1


setTimeout(function() {
    console.log(this) //window
    function fn(){
        console.log(this) //window
    }
    fn()
}, 0);

在前面加上:'use strict'

综上:普通函数中,在非严格模式下this指向全局对象window,严格模式下禁止指向全局对象,及this为undefined。

2、对象方法中的this

var a = 1
var obj1 = {
    a:2,
    fn:function(){
        console.log(this.a)
    }
}
obj1.fn() //输出2--this指向调用函数的对象

var fn = obj1.fn;
fn() //输出1--相当于普通函数

setTimeout(obj1.fn, 0);  //输出1--和上面一种一样,普通函数调用

(<object>.<function>())-》作为对象的方法调用:this指向调用者<object>。

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};
xiaoming.age(); // 25, 正常结果
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
getAge(); // NaN


var fn = xiaoming.age; // 先拿到xiaoming的age函数 
fn(); // NaN

 如下改版:

'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - this.birth;
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined

原因:this指针只在age方法的函数内指向xiaoming,在函数内部定义的函数,this又指向undefined了!(在非strict模式下,它重新指向全局对象window!)

修改如上写法,使其this仍然指向当前对象,是在当前对象的方法上保存this:

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var that = this; // 在方法内部一开始就捕获this
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - that.birth; // 用that而不是this
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // 25

改成箭头函数:

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    }
};
obj.getAge(); // 25
//注意箭头函数还是需要通过对象调用
var func = obj.getAge
func(); //NaN

箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj:

注意

  • 由于箭头函数this的指向,所以以前的那种hack写法就不需要了:var that = this;
  • 由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2015); // 25

3、dom

4、构造函数中的this

构造函数调用模式(obj = new <function>()):this指向被构造者obj。

没有return或return普通类型,那么this指向o,所以this.a=37,就是o.a=37即给o添加新属性;return 对象,new之后得到的o就指向这个对象,所以o={a:38}

5、apply/call/bind方法:this指向方法的第一个参数。

call/apply方法中的this:动态修改函数中的this指向,第一个参数作为this的指向.

apply/call:this指向第一个参数,但是若是传null和undefined,在非严格模式下则指向全局对象,即在浏览器中指向window,严格模式下不允许指向全局对象,即为null/undefined

   

6、严格模式下,this不允许指向全局对象,此时this值为undefined。

总结

  • this的4种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定,分别对应函数的四种调用方式:独立调用、方法调用、间接调用和构造函数调用。
  • this绑定优先级:new绑定>显式绑定>隐式绑定>默认绑定

1)默认绑定:全局环境中,this默认绑定到window

  • 全局环境中,this默认绑定到window
console.log(this === window);//true
  • 函数独立调用时,this默认绑定到window
function foo(){
    console.log(this === window);
}
foo(); //true
  • 被嵌套的函数独立调用时,this默认绑定到window
//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
var a = 0;
var obj = {
    a : 2,
    foo:function(){
            function test(){
                console.log(this.a);
            }
            test();
    }
}
obj.foo();//0
  • IIFE立即执行函数实际上是函数声明后直接调用执行
var a = 0;
function foo(){
    (function test(){
        console.log(this.a);   //this指向window
    })()
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo();//0
  • 闭包中内部函数的this默认指向window对象
var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//0

由于闭包的this默认绑定到window对象,但又常常需要访问嵌套函数的this,所以常常在嵌套函数中使用var that = this,然后在闭包中使用that替代this,使用作用域查找的方法来找到嵌套函数的this值 

2)隐式绑定

隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。使用对象调用函数进行绑定( obj.fun() ) ,执行obj.fun(),obj就是上下文环境。注意:隐式绑定可能丢失。


function foo(){
    console.log(this.a);
};
var obj1 = {
    a:1,
    foo:foo,
    obj2:{
        a:2,
        foo:foo
    }
}
 
//foo()函数的直接对象是obj1,this隐式绑定到obj1
obj1.foo();//1
 
//foo()函数的直接对象是obj2,this隐式绑定到obj2
obj1.obj2.foo();//2

隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这种情况容易出错却又常见

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar();//0

3)显式绑定

通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。对于被调用的函数来说,叫做间接调用

var a = 0;
function foo(){
    console.log(this.a);
}
var obj1 = {
    a:1
};
var obj2 = {
    a:2
};
foo.call(obj1);//1


var bar = function() {
	foo.call( obj2 );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2

javascript中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()

var id = 'window';
function foo(el){
    console.log(el,this.id);
}
var obj = {
    id: 'fn'
};
[1,2,3].forEach(foo);//1 "window" 2 "window" 3 "window"
[1,2,3].forEach(foo,obj);//1 "fn" 2 "fn" 3 "fn"

注意:箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

function foo() {
	setTimeout(() => {
		// 这里的 this 在此法上继承自 foo()
		console.log( this.a );
	},100);
}
var obj = {
	a:2
};
foo.call( obj ); // 2

例外:

function foo() {
	console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2 仅仅是把o.foo传递给p.foo,并不会把o对象传入

4)new绑定

如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。理解new一个对象发生的过程。this指向创建的新对象。

function fn(){
    this.a = 2;
}
var test = new fn();
console.log(test.a);//2


//优先级探索1
//1.显式绑定和隐式绑定的优先级?
function foo() {
	console.log( this.a );
}
var obj1 = {
	a: 2,
	foo: foo
};
var obj2 = {
	a: 3,
	foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3 可以看到显式绑定将隐式绑定的this给更改了
obj2.foo.call( obj1 ); // 2 可以看到显式绑定将隐式绑定的this给更改了


//优先级探索2
//隐式绑定和new绑定的优先级?
function foo(something) {
	this.a = something;
}
var obj1 = {
	foo: foo
};
obj1.foo( 2 );
console.log( obj1.a ); // 2
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4 new绑定后创建了一个新对象对this进行绑定,new的优先级更高


//优先级探索3
//显式绑定和new绑定的优先级?
function foo(something) {
	this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3 new能将this绑定到自身新建的对象上,new的优先级更高

二、区别

1、区别1

var xw = {
        name : "小王",
        gender : "男",
        age : 24,
        say : function() {
              alert(this.name + " , " + this.gender + " ,今年" + this.age);
        }
}
xw.say();   //小王 , 男 ,今年24


var xh = {
        name : "小红",
        gender : "女",
        age : 18
}

修改say方法的this指向为xh:

xw.say.call(xh);
xw.say.apply(xh);
xw.say.bind(xh)();

从上面可以看出,bind与apply、call最大的区别:bind不会立即调用,其他两个会立即调用=》bind返回对应函数, 便于稍后调用; apply, call则是立即调用。

2、区别2

var obj = {
    message: 'My name is: '
}

function getName(firstName, lastName) {
    console.log(this.message + firstName + ' ' + lastName)
}

传参:

getName.call(obj, 'Dot', 'Dolby')
getName.apply(obj, ['Dot', 'Dolby']) // My name is: Dot Dolby

//bind
function fn(a, b, c) {
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');

fn('A', 'B', 'C');            // A B C
fn1('A', 'B', 'C');           // Dot A B
fn1('B', 'C');                // Dot B C
fn.call(null, 'Dot');      // Dot undefined undefined

apply与call传参的区别:apply传参是通过数组,call传参是按顺序传入。在确定的参数下,还是最好用call,call的效果会更高,但是在函数的延展性上使用apply更好。

1)bind方法实现函数柯里化curring

什么叫柯里化:预先设置一些参数,是指这样一个函数,它接收函数 A,并且能返回一个新的函数,这个新的函数能够处理函数 A 的剩余参数

如下案例:不想改变this,bind直接传undefined即可,传入的100,默认是给了第一个参数a,之后调用func(1,2),a已经有值了,所以b=1,c=2;第二次调用:第一次a绑定了100,第二次bind那么就把值bind到b上即b=200,那么func(10)就是c=10

   

2)bind与new

如上,直接调用函数。此时函数中的this,非严格模式下默认指向的是全局对象,严格模式下不允许指向全局就是undefined。但是使用了.bind之后就会修改其中this的指向,指向传入的第一个参数,即bind之后this指向{a:1},this.b=100为该对象添加了新属性。

new 函数构造器形式,其中this:没有return或者return返回基本类型,则会作为this返回(this指向的是空对象,该空对象的原型是foo.protytype,此时就是返回this,即空对象,但是有this.b=100,即返回的就是{b:100},注意this作为返回值忽略return),若是return 对象,则会作为函数构造器new 新对象的返回值,就此时返回的是return指定的对象。

如下涉及问题:如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?

  • 不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象。

bind是ES5新增的所以只有IE9+才支持,兼容旧版本浏览器

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
     if (typeof this !== 'function') {
     // closest thing possible to the ECMAScript 5
     // internal IsCallable function
     throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
 }
     var aArgs = Array.prototype.slice.call(arguments, 1),
     fToBind = this,
     fNOP = function() {},
     fBound = function() {
     // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
     return fToBind.apply(this instanceof fBound
     ? this
     : oThis,
     // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
     aArgs.concat(Array.prototype.slice.call(arguments)));
 };
     // 维护原型关系
     if (this.prototype) {
     // 当执行Function.prototype.bind()时, this为Function.prototype 
     // this.prototype(即Function.prototype.prototype)为undefined
     fNOP.prototype = this.prototype; 
     }
     // 下行的代码使fBound.prototype是fNOP的实例,因此
     // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
     fBound.prototype = new fNOP();
     return fBound;
 };
}

apply,call,bind三者的区别总结:

  • 三者都可以改变函数的this对象指向。
  • 三者第一个参数都是this要指向的对象,如果没有这个参数或参数为undefined或null,则默认指向全局window(非严格模式下)。
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
  • bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。

注意:在 ES6 的箭头函数下, call 和 apply 将失效, 对于箭头函数来说:

  • 箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;所以不需要类似于var _this = this这种丑陋的写法
  • 箭头函数不可以当作构造函数,也就是说不可以使用 new 命令, 否则会抛出一个错误
  • 箭头函数不可以使用 arguments 对象,,该对象在函数体内不存在. 如果要用, 可以用 Rest 参数代替
  • 不可以使用 yield 命令, 因此箭头函数不能用作 Generator 函数,什么是Generator函数可自行查阅资料,推荐阅读阮一峰Generator 函数的含义与用法Generator 函数的异步应用

三、手写call、apply、bind

 如上,后面不能用箭头函数,箭头函数没有this,是捕获上下文的this供自己使用,所以this为undefined。用普通函数,this就是调用该方法的函数对象即arr。

所以下面foo.myCall(obj, 1, 2),myCall函数中this是前面的foo函数,myCall函数的参数context和args就是后面调用括号内的参数,也可以用arguments获取。

 具体如下,bind、apply和call是有返回值的:

//手写call、apply、bind
Function.prototype.myCall = function (context, ...args) {
  // const args = [...arguments].slice(1) //排除第一个参数this指向,后面的参数都是执行的函数要接受的参数
  context = context || window;
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  //   context.fn = this; //this就是调用的函数,即本例下面的foo函数
  //   const result = context.fn(...args);
  //   delete context.fn;
  const result = context[fnSymbol](...args);
  delete context[fnSymbol]; //恢复context,把新加入的fn再删除
  return result;
};

Function.prototype.myApply = function (context, args) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  context = context || window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;

  const result = context[fnSymbol](...args);
  delete context[fnSymbol];
  //   const result = this(...args); //直接调用而不是把fn绑定到context上那么this函数中若是使用了this即获取修改this指向的obj中的属性,那么访问不了obj中的属性
  return result;
};

Function.prototype.myBind = function (context, ...args) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  context = context || window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;

  return function F(..._args) {
    args = args.concat([...arguments]);
    let result;
    if (this instanceof F) {
      //根据this绑定规则的优先级,new的优先级高于bind,所以此处若是new来的,this指向非之前的
      result = context[fnSymbol].apply(this, args);
    } else {
      result = context[fnSymbol].apply(context, args);
    }

    delete context[fnSymbol];
    return result;
  };
};

function foo(a, b) {
  console.log(a, b, this.age); //context.fn = this就是为了函数中使用this.age
  return a + b;
}

function foo2(a, b) {
  console.log(a, b, this.age); //context.fn = this就是为了函数中使用this.age
  return {
    age: 21,
  };
}

const obj = {
  age: 20,
};

// foo.myCall(obj, 1, 2);
// foo.myApply(obj, [1, 2]);
const cur = foo2.myBind(obj, 1);
// cur(2);
const o = new cur(2);
console.log(o, o.age);

上面myBind最后delete context[fnSymbol];有问题,案例bind后只执行了一次正好,再次执行cur(3),此时函数已经被删除了context[fnSymbol]再执行会报做错

 报错如下:

修改如下:里面使用了apply已经进行了this指向的绑定,所以不需要之前的操作,下面是维护原型关系:

Function.prototype.myBind = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  const fToBind = this;
  const aArgs = Array.prototype.slice.call(arguments, 1);
  function F() {
    const args = aArgs.concat(Array.prototype.slice.call(arguments));
    let result;
    if (this instanceof F) {
      //根据this绑定规则的优先级,new的优先级高于bind,所以此处若是new来的,this指向非之前的
      result = fToBind.apply(this, args);
    } else {
      result = fToBind.apply(context, args);
    }

    // delete context[fnSymbol]; //此处删除,F只能执行一次,第二次后就已context[fnSymbol]就没有了执行会报错
    return result;
  }

  function fNOP() {}
  if (this.prototype) {
    fNOP.prototype = this.prototype;
  }
  F.prototype = new fNOP();
  return F;
};

注意:context.fn = this的作用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值