JavaScript this Keyword

在执行上下文的创建阶段, 会分别生成变量对象, 建立作用域链, 确定this指向. this指向是在函数被调用的时候确定的, 也就是执行上下文被创建时确定的. 一个函数中的this指向可以非常灵活.

1
2
3
4
5
6
7
8
9
10
11
var a = 4;
var obj = {
a: 5
}

function foo() {
console.log(this.a);
}

foo(); // 4
foo.call(obj); // 5

在函数执行过程中, this一旦被确定, 就不可更改.

1
2
3
4
5
6
7
8
9
10
11
var a = 4;
var obj = {
a: 5
}

function foo() {
this = obj; // 试图修改this, 运行时会报错
console.log(this.a);
}

foo();

全局对象中的this

全局对象对象中的this, 指向它本身.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过this绑定到全局对象
this.a = 'zero';

// 通过声明绑定到变量对象, 全局环境中变量对象就是它本身
var b = 'one';

// 赋值操作, 标识符会隐式绑定到全局对象
c = 'two';

// 打印日志
console.log(a);
console.log(b);
console.log(c);

函数中的this

1
2
3
4
5
// the first demo
var a = 10;
function bar() {
console.log(this.a);
}
1
2
3
4
5
6
7
8
9
// the second demo
var a = 10;
function bar() {
function foo() {
console.log(this.a);
}
foo();
}
bar();
1
2
3
4
5
6
7
8
9
10
11
// the third demo
var a = 10;
var obj = {
a: 50,
b: this.a + 10,
foo: function () {
return this.a;
}
};
console.log(obj.b);
console.log(obj.foo());

想要准确确定this指向, 找到函数的调用者以及区分它是否独立调用就变得十分关键. 在一个函数上下文中, this由调用者提供, 由调用函数的方式来决定. 如果调用者函数被某一个对象所拥有, 那么该函数在调用时, 内部的this指向该对象, 如果函数独立调用, 那么该函数内部的this, 则指向undefined. 但在非严格模式中, this指向undefined时, 它会被自动指向全局对象.

1
2
3
4
5
6
7
// 非严格模式this会自动指向全局, 我们在函数内部使用严格模式获得更准确的判断
function bar() {
'use strict';
console.log(this);
}
bar(); // bar是调用者, 独立调用
window.bar(); // bar是调用者, 被window对象所拥有

在上面的例子中, bar()作为独立调用者, 按照定义的理解, 它内部的this指向undefined. 而window.bar()因为fn()方法被window对象所拥有, 内部的this指向window对象.

the third demo中, 对象obj的b属性使用this.a + 10来计算. 单独的{}不会形成新的作用域的, 因此这里的this.a, 由于没有作用域的限制, 它仍处于全局作用域中, 所以这里的this其实指向的是window对象.

1
2
3
4
5
6
7
8
9
10
11
var a = 5;
var foo = {
a: 8,
bar: function () {
return this.a;
}
}
console.log(foo.bar()); // 8

var bingo = foo.bar();
console.log(bingo()); // 5

foo.bar()中, bar是调用者, 它不是独立调用, 被对象foo所拥有, 因此它的this指向foo. 而bingo()作为调用者, 尽管它与foo.bar的引用相同, 但是它是独立调用的, 因此this指向undefined, 在非严格模式中, 自动指向window全局对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 稍微修改一下代码
var a = 5;
function bar() {
return this.a;
}
var foo = {
a: 8,
bar: bar
};
console.log(foo.bar()); // 8

// 再次修改
function foo() {
console.log(this.a);
}
function bingo(fn) {
fn(); // 独立调用
}

var a = 5;
var obj = {
a: 8,
bar: foo
};

bingo(obj.bar); // 8

call,apply显式指定this指向

JavaScript内部给我们提供两种方法call()apply(), 可以手动设置this的指向. 所有函数都具有这两个方法, 除了参数略有不同, 实现功能完全一样.

1
2
3
4
5
6
7
8
9
function bar() {
console.log(this.a);
}
var obj = {
a: 10
};
// bar并不是obj的方法, 通过call方法, 将bar内部的this指向obj对象
// 通过this.a访问obj的a属性
bar.call(obj); // 10
1
2
3
4
5
6
7
8
9
10
11
function bar(a, b) {
console.log(this.c + a + b);
}
var obj = {
c: 3
};
// call()方法和apply()方法的参数:
// 第一个参数为this将要指向的对象
// 第二个参数call()方法是以单个的方式传递, apply()是以数组的方式传递
bar.call(obj, 10, 5); // 18
bar.apply(obj,[10,5]); // 18
1
2
3
4
5
6
7
8
// 将类数组对象转换为数组
function example(a, b, c, d) {
console.log(arguments);
// 使用call/apply将arguments转为数组, 返回数组, arguments不会改变
var arg = [].slice.call(arguments);
console.log(arg);
}
example('zero','one','two','three');
1
2
3
4
5
6
7
8
9
10
11
// 修改this指向
var bar = {
name: 'joker',
say: function() {
console.log(this.name + 'say hello world');
}
};
var foo = {
name: 'knight'
};
bar.say.call(foo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现继承

// 定义父类的构造函数
var Person = function (name, age) {
this.name = name;
this.age = age;
this.sex = ['male,female'];
}

// 定义子类的构造函数
var Son = function (name, age, hobby) {
Person.call(this, name, age);
this.hobby = hobby;
}

Son.prototype.message = function() {
console.log('name: ' + this.name + ', age: ' + this.age + ', sex: ' + this.sex + ', hobby: ' + this.hobby);
}

new Son('Mike', 18, 'swim').message();

在Son构造函数中使用call()方法将父级构造函数执行了一次, 相当于将Person中的代码在Son构造函数复制了一份, this指向为Son构造函数new出来的实例对象.

1
2
3
4
5
6
7
var Son = function (name, age, hobby) {
this.name = name;
this.age = age;
this.sex = ['male','female'];
// Person.call(this, name, age); 相当于执行了上面三行代码
this.hobby = hobby;
}

在向其它执行上下文的传递中, 确保this指向保持不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 我们希望bar()方法被obj对象调用时, this指向obj对象
// 但是匿名函数导致this指向丢失, 在匿名函数中this指向了全局
var obj = {
a: 8,
bar: function () {
setTimeout(fucntion () {
console.log(this.a);
},1000);
}
};
obj.bar();

// 修改上面代码
var obj = {
a: 8,
bar: function() {
// 将this的引用保存起来, 赋值给that变量
var that = this;
setTimeout(fucntion () {
console.log(that.a);
},1000);
}
};
obj.bar();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 封装bind()方法
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
}
}

var obj = {
a: 8,
bar: function () {
setTimeout(bind(function () {
console.log(this.a);
}, this), 1000);
}
};
obj.bar();

// 使用ES5中的bind()方法
var obj = {
a: 8,
bar: function () {
setTimeout(function () {
console.log(this.a);
}.bind(this), 1000);
}
};

构造函数与原型方法上的this

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.foo = function () {
return this.name;
}

var bingo = new Person('Jack', 40);
bingo.foo();

new操作符调用构造函数会经历是个阶段:

  • 创建一个新对象
  • 构造函数的this指向这个新对象
  • 指向构造函数的代码, 为这个对象添加属性和方法
  • 返回新对象

new操作符调用构造函数, this指向这个新建的对象, 然后将新对象返回, 被实例对象bingo接收.

原型方法上的this, 根据上面例子函数中的this定义, bingo.foo()中的foo为调用者, 它被bingo对象所拥有, foo中的this指向bingo对象.

参考链接