JavaScript中this关键字使用

this 关键字

含义

this都有一个共同点:它总是返回一个对象。this就是属性或方法“当前”所在的对象

使用场合

在理解this的绑定过程之前,首先要理解调用位置,调用位置就是函数在代码中被调用的位置

1. 全局环境 (默认绑定)

全局环境中的this,就是顶层对象window

TIM图片20180408192539.png

上面的代码就是说明不管是不是在函数内部,只要在全局环境下运行,this就是window对象
所以最常用的函数调用类型:独立的函数调用可以把这条规则看作是无法应用其他规则时的默认规则

function foo(){
    console.log(this.a)
}
var a = 3;
foo() //3
image.png

因为this.a被解析成了全局变量a,因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象

如何判定这里应用了默认绑定呢,可以通过分析调用位置来看foo()是如何调用的,foo()是直接使用不带任何修饰符的函数引用进行调用的,因此只能使用默认绑定

如果使用严格模式则不能将全局对象用于默认绑定,因此this会绑定到undefined;

function foo(){
    'use strict'
    console.log(this.a);
}
var a = 3;
foo();
//

虽然this的绑定规则完全取决于调用位置,但是只是foo()运行在非strict mode下时,默认绑定才能绑定到全局对象,在严格模式下调用foo()则不影响默认绑定

2. 对象的方法

如果对象的方法里面包含this,this的指向就是方法运行时所在的对象,该方法赋值给另一个对象,就会改变this的指向。
就是调用的位置是否有上下文对象,或者说是否被某个对象拥有或者包含

var obj ={
  foo: function () {
    console.log(this);
  }
};

obj.foo() // obj
obj.foo方法指向时,它内部的this指向obj

function foo(){
    console.log(this.a)
}
var obj = {
    a:2,
    foo:foo
}
obj.foo(); //2
当foo()被调用时,他的前面确实加上了对obj的引用,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因为调用foo()时this被绑定到obj,因此this.a和obj.a 是一样的

对象属性引用链中只有一层或者说最后一层在调用位置中起作用

function foo(){
    console.log(this.a)
}
var obj2 = {
    a:42,
    foo:foo
}
var obj1 = {
    a:2,
    obj2:obj2
};
obj1.obj2.foo()// 42

但是,下面这几种用法,都会改变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指向全局环境。上面三种情况等同于下面的代码。

// 情况一
(obj.foo = function () {
  console.log(this);
})()
// 等同于
(function () {
  console.log(this);
})()

// 情况二
(false || function () {
  console.log(this);
})()

// 情况三
(1, function () {
  console.log(this);
})()
image.png

虽然bar是obj.foo的一个引用,但是实际上,他引用的只是foo函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

要特别注意下回调函数

function foo(){
    console.log(this.a)
}
function defoo(fn){
    fn();
}
var obj = {
    a:2,
    foo:foo
}
var a = "lijunhui";
defoo(obj.foo)
//lijunhui

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
就相当于是

var fn = obj.foo
fn()

如果把函数传入语言内置的函数而不是传入自己声明的函数,会发生什么呢,结果是一样的,没有区别:

image.png

javascript 环境中内置的setimeout()函数实现和下面的伪代码类似
function settimeout(fn,delay){
//等待delay毫秒
fn(); 调用位置;
}

避免多层的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的变量

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

o.f1()
// Object
// Object

因为定义了变量that固定指向外层的this,然后在内层使用that 就不会发生this的指向的改变。

避免数组处理方法中的this

数组的map和foreach方法,允许提供一个函数作为参数,这个函数内部不应该使用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

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

o.f()
// hello a1
// hello a2

另一种方法是将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

3. 绑定this的方法

this的动态切换,JavaScript提供了call,apply,bind这三个方法来,来切换/固定this的指向

Function.prototype.call()

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

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f。
call方法的参数,应该是一个对象。如果参数为空,null和undefined,则默认传入对象。
《JavaScript权威指南》这样定义的call,apply
call()和apply() 的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对他的引用,要想以对象o的方法来调用函数f() 这样可以使用call()和apply()

f.call(o)
f.apply(o)

call()和apply() 的一个实参都会变为this的值,哪怕传入的实参是原始值甚至是null或者undefined,传入的null和undefined都会被全局对象代替,而其他原始值则会被相应的包装对象所代替。
如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。

var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}
上面代码中,call的参数为5,不是对象,会被自动转成包装对象(Number的实例),绑定f内部的this。

其实我自己的理解,就是使用call或者apply方法,传入的对象就是要调用的上下文,比如有一个对象obj,有一个函数foo(),你想通过obj对象调用foo()函数,就可以通过
foo.calll(obj); 而foo函数中this的值就绑定到obj这个对象上了。具体可以看如下代码

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


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方法没有参数,或者参数为null或undefined,则等同于指向全局对象。

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

4. 构造函数

典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

var Person = function(){
    this.speak = "I am a boy";
}

其实这个函数就是构造函数,为了与普通函数做区别,构造函数的第一个字母要大写
构造函数的特点有两个

  1. 函数体内部使用了this关键字,代表了所要生成的对象实例
  2. 生成对象的时候,必须使用new 命令

new 命令

new 命令的作用就是执行构造函数,返回一个实例对象

var Vehicle = function () {
  this.price = 1000;
};

var v = new Vehicle();// 也可以这样写var v = new Vehicle;
v.price // 1000

new 命令的作用就是执行Vehicle 所以可以不用写()让构造函数Vehicle生成一个实例对象,保存在变量v中,这个实例对象,从构造函数Vehicle得到了price属性,new命令执行时,构造函数内部中的this就代表了新生成的实例对象,this.price表示实例对象有一个price属性,值是1000

使用new 命令时,根据需要,构造函数也可以接受参数

var Vehicle = function (p) {
  this.price = p;
};

var v = new Vehicle(500);

new命令的原理

使用new命令时,它后面的函数一次执行下面的步骤
1.创建一个空对象,作为将要返回的对象实例
2.将这个空对象的原型,指向构造函数的prototype属性
3.将这个空对象赋值给函数内部的this关键字
4.开始执行构造函数内部的代码
5.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上,所以构造函数之所以叫“构造函数”就是说这个函数的目的,就是操作一个空对象(this对象)将其构造为需要的样子

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000
// false

上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。

image.png

但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。

var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};

(new Vehicle()).price
// 2000
image.png

上面代码中,构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。

另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。

function getMessage() {
  return 'this is a message';
}

var msg = new getMessage();

msg // {}
typeof msg // "object"

上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句。

5.Object.create()创建实例对象

构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法。 JavaScript 提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

var person1 = {
  name: '张三',
  age: 38,
  greeting: function() {
    console.log('Hi! I\'m ' + this.name + '.');
  }
};

var person2 = Object.create(person1);

person2.name // 张三
person2.greeting() // Hi! I'm 张三.

上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法。

// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 实例对象
var B = Object.create(A);

Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
B.__proto__ == A
true

上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

实际上,Object.create方法可以用下面的代码代替。

if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

上面代码表明,Object.create方法的实质是新建一个空的构造函数F,然后让F.prototype属性指向参数对象obj,最后返回一个F的实例,从而实现让该实例继承obj的属性。

下面三种方式生成的新对象是等价的。

var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
如果想要生成一个不继承任何属性(比如没有toString和valueOf方法)的对象,可以将Object.create的参数设为null。
var obj = Object.create(null);

obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'

上面代码中,对象obj的原型是null,它就不具备一些定义在Object.prototype对象上面的属性,比如valueOf方法。

使用Object.create方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。

Object.create()
// TypeError: Object prototype may only be an Object or null
Object.create(123)
// TypeError: Object prototype may only be an Object or null

object.create方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。

var obj1 = { p: 1 };
var obj2 = Object.create(obj1);

obj1.p = 2;
obj2.p // 2

上面代码中,修改对象原型obj1会影响到实例对象obj2。

除了对象的原型,Object.create方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。

var obj = Object.create({}, {
  p1: {
    value: 123,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  p2: {
    value: 'abc',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});

// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';

Object.create方法生成的对象,继承了它的原型对象的构造函数。

function A() {}
var a = new A();
var b = Object.create(a);

b.constructor === A // true
b instanceof A // true
上面代码中,b对象的原型是a对象,因此继承了a对象的构造函数A。

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

推荐阅读更多精彩内容

  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,108评论 0 21
  • 涵义 this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。 首先,thi...
    许先生__阅读 555评论 0 4
  • 第2章 基本语法 2.1 概述 基本句法和变量 语句 JavaScript程序的执行单位为行(line),也就是一...
    悟名先生阅读 4,135评论 0 13
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,550评论 0 5
  • 北方的冬天又到来了。 我极度怕冷,所以向来不喜欢冬天。不过此刻我满怀希望,期待着一个飘雪冬夜的到来。 从前,我总觉...
    舟与鱼阅读 465评论 1 2