this call apply bind

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

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对象。


<input type="text" name="age" size=3 onChange="validate(this, 18, 99);">

<script>
function validate(obj, lowval, hival){
  if ((obj.value < lowval) || (obj.value > hival))
    console.log('Invalid Value!');
}
</script>

如上,我们在输入框进行输入时onchange事件会执行,但是所传入的this指向的却是当前的文本对象也就是input框。通过this.value获取输入框的值。


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()创建对象是第二个参数会以描述对象的形式书写。
关于Object对象https://www.jianshu.com/p/0846f6642769


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

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

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

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

如上,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

   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固定指向某个对象,减少不确定性


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

Function.prototype.call();

函数实例的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的第一个参数是需要绑定的对象,从第二个参数开始便是运行函数所需要的参数

    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();

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做一些特殊的事情。
1.找出数组最大元素

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

2.将数组的空元素转换为undefined

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

空元素与undefined的区别是我们在forEach时空元素会被跳过,undefined则会被读为undefined。
3.转换疑似数组的对象

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属性。

4.绑定回调函数的对象。

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绑定需要注意:
1.每次绑定都会返回一个方法,会产生一些问题。例如事件绑定时

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);

2.结合回调函数使用

var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};
function callIt(callback) {
  callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350