JavaScript之 this 关键字

JavaScript 面向对象编程的基础知识篇 2 。

1. 概述

在上篇文章 JavaScript之new命令 中提到, this 可以用在构造函数之中,表示实例对象。
此外,this 还可以用在别的场合。但不管是什么场合,this 都有一个共同点:它总是返回一个对象

简单说,this 就是属性或方法 “当前” 所在的对象。

下面是一个实际的例子。

var person = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

person.describe()
// "姓名:张三"

上面代码中,this.name 表示 name 属性所在的那个对象。由于this.name是在 describe 方法中调用,而 describe 方法所在的当前对象是person,因此 this指向 personthis.name 就是person.name

只要函数被赋给另一个变量,this 的指向就会变。

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var name = '李四';
var f = A.describe;
f() // "姓名:李四"

上面代码中,A.describe 被赋值给变量f,内部的this 就会指向f 运行时所在的对象(本例是顶层对象)。

2. this 的实质

var obj = { foo:  5 };

上面的代码将一个对象赋值给变量 obj 。JavaScript 引擎会先在内存里面,生成一个对象 { foo: 5 },然后把这个对象的内存地址赋值给变量 obj 。也就是说,变量obj 是一个地址(reference)。后面如果要读取 obj.foo,引擎先从obj 拿到内存地址,然后再从该地址读出原始的对象,返回它的foo 属性。

由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this 就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的 this.x 就是指当前运行环境的 x

3. 使用场合

this 主要有以下几个使用场合。

(1)全局环境

全局环境使用 this,它指的就是顶层对象 window

this === window // true

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

上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this 就是指顶层对象window

(2)构造函数

构造函数中的 this,指的是实例对象。

var Obj = function (p) {
  this.p = p;
};

上面代码定义了一个构造函数 Obj 。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个 p 属性。

(3)对象的方法

如果对象的方法里面包含 thisthis 的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变 this 的指向。

4. 使用注意点

4.1 避免多层 this

由于 this 的指向是不确定的,所以切勿在函数中包含多层的 this

var o = {
  f1: function () {
    console.log(this);
    var f2 = function () {
      console.log(this);
    }();
  }
}

o.f1()
// Object
// Window

上面代码包含两层 this,结果运行后,第一层指向对象 o,第二层指向全局对象,因为实际执行的是下面的代码。

var temp = function () {
  console.log(this);
};

var o = {
  f1: function () {
    console.log(this);
    var f2 = temp();
  }
}

事实上,使用一个变量固定 this 的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。

JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的 this 指向顶层对象,就会报错。

var counter = {
  count: 0
};
counter.inc = function () {
  'use strict';
  this.count++
};
var f = counter.inc;
f()
// TypeError: Cannot read property 'count' of undefined

上面代码中,inc 方法通过 'use strict' 声明采用严格模式,这时内部的this 一旦指向顶层对象,就会报错。

4.2 避免数组处理方法中的 this

数组的 mapforeach 方法,允许提供一个函数作为参数。这个函数内部不应该使用 this

4.3 避免回调函数中的 this

回调函数中的 this 往往会改变指向,最好避免使用。

可以采用下面的一些方法对 this 进行绑定,就是使得 this 固定指向某个对象,减少不确定性。

5. 绑定 this 的方法

JavaScript 提供了 callapplybind这三个方法,来切换/固定 this 的指向。

5.1 Function.prototype.call()

函数实例的 call 方法,可以指定函数内部this 的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

call 方法的参数,应该是一个对象。如果参数为空、nullundefined,则默认传入全局对象。

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

上面代码中,a 函数中的 this 关键字,如果指向全局对象,返回结果为123 。如果使用call 方法将 this 关键字指向 obj 对象,返回结果为456
可以看到,如果call 方法没有参数,或者参数为nullundefined,则等同于指向全局对象。

call 方法还可以接受多个参数。

func.call(thisValue, arg1, arg2, ...)

call 的第一个参数就是 this 所要指向的那个对象,后面的参数则是函数调用时所需的参数。

function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3

上面代码中,call 方法指定函数 add 内部的 this 绑定当前环境(对象),并且参数为 12,因此函数 add 运行后得到 3

5.2 Function.prototype.apply()

5.2.1 方法

apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])

apply 方法的第一个参数也是 this 所要指向的那个对象,如果设为nullundefined,则等同于指定全局对象。
第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。
原函数的参数,在call方法中必须一个个添加,但是在 apply 方法中,必须以数组形式添加。

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

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
5.2.2 应用
(1)找出数组最大元素

JavaScript 不提供找出数组最大元素的函数

结合使用apply方法和Math.max方法,就可以返回数组的最大元素。

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
(2)将数组的空元素变为 undefined

通过 apply 方法,利用 Array 构造函数将数组的空元素变成undefined

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

空元素与 undefined 的差别在于,数组的forEach 方法会跳过空元素,但是不会跳过 undefined

var a = ['a', , 'b'];

function print(i) {
  console.log(i);
}

a.forEach(print)
// a
// b

Array.apply(null, a).forEach(print)
// a
// undefined
// b
(3)转换类似数组的对象

利用数组对象的 slice 方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组。

(4)绑定回调函数的对象

由于 apply 方法(或者 call 方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。

5.3 Function.prototype.bind()

5.3.1 方法

bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数。

bind 方法的参数就是所要绑定 this 的对象。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1

上面代码中,counter.inc方法被赋值给变量func 。这时必须用bind方法将inc内部的this,绑定到counter,否则就会出错。

如果bind方法的第一个参数是nullundefined,等于将this绑定到全局对象,函数运行时this指向顶层对象(浏览器为window)。

function add(x, y) {
  return x + y;
}

var plus5 = add.bind(null, 5);
plus5(10) // 15

上面代码中,函数add 内部并没有this,使用bind方法的主要目的是绑定参数x,以后每次运行新函数 plus5,就只需要提供另一个参数 y 就够了。

5.3.2 使用注意点
(1)每一次返回一个新函数

bind 方法每运行一次,就返回一个新函数,这会产生一些问题。

element.addEventListener('click', o.m.bind(o));
element.removeEventListener('click', o.m.bind(o));  // 无法取消绑定

//正确的写法:
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

上面代码中,callIt方法会调用回调函数。这时如果直接把counter.inc传入,调用时counter.inc内部的 this 就会指向全局对象。使用 bind 方法将counter.inc绑定counter以后,就不会有这个问题,this总是指向counter

(3)结合call方法使用
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

上面的代码中,数组的slice方法从[1, 2, 3]里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]上面调用Array.prototype.slice 方法,因此可以用call 方法表达这个过程,得到同样的结果。

call 方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

上面代码的含义就是,将Array.prototype.slice 变成 Function.prototype.call 方法所在的对象,调用时就变成了Array.prototype.slice.call

参考链接

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

推荐阅读更多精彩内容

  • 涵义 this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。 首先,thi...
    许先生__阅读 524评论 0 4
  • 转载自:https://wangdoc.com/javascript/oop/this.html 1,涵义 thi...
    团子家族_方糖咖啡阅读 493评论 0 1
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,348评论 0 5
  • 1.含义 this就是属性或方法“当前”所在的对象。 上面代码中,this.name表示name属性所在的那个对象...
    Kevin丶CK阅读 771评论 0 3
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,152评论 0 4