this apply call bind


在JS中,一切事物皆对象,运行环境也是对象,就连方法也是对象,在最底层有windows对象。可以说所有的全局变量,方法都是挂载在window对象上的。
当然window也有其他的一些属性alert、confirm等。

JS中this的动态绑定,虽然为JS带来了一定的灵活性,但也为初级开发者带来了一定的难度,初学者可能无法快速的定位到this的上下文。有时候我们需要将this固定下来,JS提供了call、apply、bind三个方法专门用于this指定

this

this.property // window

如上,返回window对象,this指的就是property属性所运行的环境对象。

const person = {
    name:'ccg',
    dothing(){
        console.log(this.name);
    }
}
person.dothing();//ccg

如上,this.name在dothing中进行执行,所以this便指向了dothing的运行环境person。


然而this是可变的,可以进行更改指向的

const person = {
    name:'ccg',
    dothing(){
        console.log(this.name);
    }
}
const person2 = {name:'ccg2'};
person2.dothing = person.dothing;
person2.dothing();//ccg2

如上,我们将person2的dothing事件指向了person.dothing。运行环境便是person2,所以执行结果是ccg2。

下面是上面例子的拆解,便于理解

    const dothing = function () {
        console.log(this.name);
    }
    const person = {
        name: 'ccg',
        dothing
    }
    const person2 = { name: 'ccg2' };
    person2.dothing = person.dothing;
    person2.dothing();//ccg2

注意:函数只要进行变量赋值,那么函数内的this便会更改。

const person = {
    dothing:function(){
      console.log(this);
    }
}
const func = person.dothing;
func();//window

如上,我们将person.dothing事件赋值给了func,因为func的调用方是window,也就是func的运行环境是window,所以它内部的this就是window对象。

this的实质

JS之所以有this这个概念,跟内存存储机制有关。

const obj = {name:'ccg'};

如上,一个简单的obj对象。按照语法理解,似乎我们是先声明一个变量obj,然后给obj了一个对象值,其实不然。在JS中是先在内存中创建一个{name:‘ccg’},然后将内存地址赋值给obj,那么obj这个时候就指向了{name:‘ccg’}。也就是说,obj其实就是一个内存地址。

原始的对象保存是以字典结构保存,每一个属性都有自己的描述对象。如下:

{
  name: {
    [[value]]: 'ccg'
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

对象的每个属性都可以进行描述属性修改,这就是为什么我们使用Object.create()创建对象时第二个参数会以描述对象的形式书写。


这样的数据结构似乎看起来更加清晰,但问题是,对象属性有可能是一个方法。

{
  name: {
    [[value]]: 函数地址
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

如上,JS需要在内存中创建一个方法,然后将方法地址绑定给name.value
函数是一个单独的值,JS是允许更改函数的上下文环境的。

function f(){}
const obj = {f}
f();//window
obj.f()//obj

如上,f是一个方法,我们允许f单独执行,则它的运行环境是window。在obj.f()中它的运行环境便是obj。
那么我们需要一个变量来指明我所运行的上下文,便于我获取我需要的方法或变量,这个时候就有了this关键字,它的目的就是指明当前环境的上下文。

this的运行环境

全局环境

console.log(this === window); //true
function func(){
    console.log(this === window); 
}
func(); //true

以上代码说明不管实在函数内部还是在外部,只要实在全局变量下,所以的this都指向顶层对象window。

构造函数

在构造函数中this指的是当前实例对象。

function Person(name){this.name = name};
Person('ccg');
const p = new Person('ccg2');
console.log(name); //ccg
conso.log(p.name); //ccg2

如上,Person是一个简单的构造函数。我们将Person当方法调用时,this指向window,便会声明一个全局变量name。我们将Person当构造函数时,this便指向当前实例p,p便有了自己的属性name。

对象的方法

我们都知道,在对象的方法内,this默认是指向当前对象的。但是下列几种比较特殊:

// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window

上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj。

可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码。

const a = {
    aName:'a',
    b:{
      f:function(){
        conso.log(this.aName);'    
      }
    }
}
a.b.f(); //undefined

如上,this指向的a.b并不能指向a
我们要么在a.b中添加变量,要么使用变量赋值进行更改this的上下文。

this注意点

不要使用多重this

由于this是不确定的,尽量不要在方法中使用多重this

const obj = {
   name:'',
   func:function(){
       (function test(){
           console.log(this);
       })()
   }
}
obj.func(); //window

如上,在obj.func中添加一个自执行函数,在自执行函数中this便指向了window,因为自执行函数的调用方式window。
一个解决办法就是在obj.func声明that = this;然后早自执行函数值使用that进行调用obj。但是当我们嵌套多层时需要创建很多个变量来指明上下文,是一件很麻烦的事情。
或者我们可以使用JS的严格模式防止this指向window,在严格模式中禁止this指向window,会报错。

避免在数组处理方法中使用this。
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    });
  }
}
o.f()
// undefined a1
// undefined a2

上面代码中,foreach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。原因跟上一段的多层this是一样的,就是内层的this不指向外部,而指向顶层对象。

解决这个问题的一种方法,就是前面提到的,使用中间变量固定this。
另一种方法是将this当作foreach方法的第二个参数,固定它的运行环境。如下

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f()
// hello a1
// hello a2
避免回调中使用this
var o = new Object();
o.f = function () {
  console.log(this === o);
}

// jQuery 的写法
$('#button').on('click', o.f);

上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的 DOM 对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。

为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性

Function.prototype.call()

修改this指向

函数实例的call方法可以为函数指定运行的作用于,即函数内部this的指向。

function func(){
    console.log(this);
}
const obj = {};
func(); //window
func.call(obj);// obj

如上,func是一个全局方法,obj是一个全局变量,我们通过call方法将obj作为参数传递,在func中this便指向了obj。
注意:call方法会在我们设置时自执行。

call方法的参数,是应该是一个对象,当我们没传或者传递null、undefined时,参数默认是window也就是全局对象。如下:

var n = 123;
var obj = { n: 456 };
function a() {
  console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

如果call中参数是一个原始值,数字或者字符串或者Boolean等,默认会包装成对象传给call

call()参数

call的第一个参数是需要绑定的对象,从第二个参数开始便是运行函数所需要的参数

function func(a, b) {
    this.name = 'test';
    console.log(a + b)
};
const obj = { func };
func.call(obj, 1, 2); //3
console.log(obj); //test

如上,我们可以将func定义为一个公共方法来处理我们需要处理的数据对象。

call方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false

如上,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。

Function.prototype.apply()

修改this指向

apply的使用方法基本和call一致,也是用来改变函数运行上下文的方法,区别是apply只有两个参数,第一个参数是需要绑定的Objec,第二个参数是一个数组,数组由函数需要的参数构成。

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

因为apply第二个参数需要穿数组,所以我们可以使用apply做一些特殊的事情。

找出数组最大元素

const a = [1,2,45,2,3,5,2314,12,4];
Math.max.apply(null,a); //2314

将数组的空元素转换为undefined

Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]

空元素与undefined的区别是我们在forEach时空元素会被跳过,undefined则会被读为undefined。

转换疑似数组的对象

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]

利用数组的slice属性可以装类似于数组的对象转换成数组(例如arguments)
从上面的代码可以看出,这个方法运行的前提是对象必须有length属性。

绑定回调函数的对象。

var o = new Object();
o.f = function () {
  console.log(this === o);
}
var f = function (){
  o.f.apply(o);
  // 或者 o.f.call(o);
};
// jQuery 的写法
$('#button').on('click', f);

Function.prototype.bind()

apply和call方法可以用来改变this的指向,但是它们是立即执行函数,也就是我们放什么时候绑定,什么时候就会自执行一次。
更简洁的办法就是使用bind绑定,bind的传参与call一致,但是它不会立即执行

const obj = {
    name:'ccg',
    func:function(){
        'use strict'
        console.log(this.name);
    }
}
const print = obj.func;
print();//Uncaught TypeError: Cannot read property 'name' of undefined

如上,我们在将obj.func作为变量进行赋值给print,再执行print会报错,因为在print中this指向的是window,没有name属性。

const print = obj.func.bind(obj);
print(); //ccg

当我们使用bind进行绑定之后便可以使用。且不会立即执行。

bind需要注意

每次绑定都会返回一个方法,会产生一些问题。例如事件绑定时

element.addEventListener('click', o.m.bind(o));
element.removeEventListener('click', o.m.bind(o)); //无效

如上,我们在事件绑定时使用bind,会生成一个匿名函数,并且我们没办法取消绑定。
但是我们可以将事件写成一个变量,如下:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);

结合回调函数使用

var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};
function callIt(callback) {
  callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值