勿忘初心

Js函数作用域与this的指向问题

以前对函数作用域以及this指向都没有一个系统的认识,偶然一次代码编写运行结果与自己预想结果不一样,这让我决定对Js的作用域和this深层次学习。下面是我当初的一个小错误:

1
2
3
4
5
6
7
8
9
var obj = {
a: 20,
getA: function() {
setTimeout(function() {
console.log(this.a)
}, 1000)
}
}
obj.getA();

这里输出的undefined,因为这里的this指向window。只需要修改this指向就可以,方法很多。

1
2
3
4
5
6
7
8
var obj = {
a: 20,
getA: function() {
setTimeout(function() {
console.log(this.a)
}.bind(this), 1000)
}
}

首先需要了解这些重要概念:

  • 基础数据类型与引用数据类型
  • 内存空间
  • 垃圾回收机制
  • 执行上下文
  • 变量对象与活动对象

作用域

  • JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。

  • 作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。下面请允许我盗一张图:来自波同学。

  • 我理解作用域是:函数名以及变量或者anguments所能访问或查找的规则,或者叫所能访问到内存区块。优先级是anguments->function表达式->var。变量在声明它们的函数体内(或嵌套函数内)任意地方都是有定义的。下面看一份代码:
1
2
3
4
5
6
7
var scope="global";
function t(){
console.log(scope);
var scope="local"
console.log(scope);
}
t();

第一句输出的是: “undefined”,而不是 “global” 第二讲输出的是:”local” 你可能会认为第一句会输出:”global”,因为代码还没执行var scope=”local”,所以肯定会输出“global”。 上面已经讲过,变量在函数体内任意地方都是有定义的。在函数作用域创建阶段,首先会先生成变量对象VO:VO包括anguments、function、var变量的创建。到了函数执行阶段包括变量开始赋值、执行上下文this确定、代码执行。在第一句console.log打印时,函数作用域内只有var scope,scope并没有赋值。可以理解为下面代码:

1
2
3
4
5
6
7
8
var scope="global";
function t(){
var scope;
console.log(scope);
scope="local"
console.log(scope);
}
t();

在此看来,由于函数作用域的特性,局部变量在整个函数体始终是由定义的,我们可以将变量声明”提前“到函数体顶部,同时变量初始化还在原来位置。

作用域链

这里来看一下执行上下文的生命周期:从创建–>执行代码–>执行完毕,出栈等待被回收。 执行上下文生命周期

这里可以将作用域和作用域链结合起来理解:作用域是一块能够查询到的内存区域或者查询规则;作用域链,是对查询规则的实现或者内存寻址的过程,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();
//innerTest的执行上下文创建阶段如下:
innerTestEC = {
VO: {...}, // 变量对象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
this: {}
}

因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object

顺便理解一下闭包,简单来说,假设函数A在函数B的内部进行定义了,并且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。概述为:当函数可以记住并访问所在的作用域(全局作用域除外)时,就产生了闭包,即使函数是在当前作用域之外执行。有一下特点:

  • 闭包与作用域链息息相关。
  • 闭包是在函数执行过程中被确认。
  • 通过闭包,我们可以在其他的执行上下文中,访问到其函数的内部变量

闭包案例:利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5

1
2
3
4
5
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}

简单修改为:(相信你可以更多其他方法)

1
2
3
4
5
for (var i=1; i<=5; i++) {
setTimeout( (function() {
console.log(i);
})(i), i*1000 );
}

最后输出的7是setTimeout计时器的ID,计时器一般会生成一个唯一id,用于清除时用。

this的指向理解

this的指向,是在函数被调用的时候确定的

全局对象中的this

关于全局对象的this,我之前在总结变量对象的时候提到过,它是一个比较特殊的存在。全局环境中的this,指向它本身。因此,这也相对简单,没有那么多复杂的情况需要考虑。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过this绑定到全局对象
this.a2 = 20;
// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a1 = 10;
// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 30;
// 输出结果会全部符合预期
console.log(a1);
console.log(a2);
console.log(a3);

函数中的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
25
26
// demo01
var a = 20;
function fn() {
console.log(this.a);
}
fn();
// demo02
var a = 20;
function fn() {
function foo() {
console.log(this.a);
}
foo();
}
fn();
// demo03
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
console.log(obj.c);
console.log(obj.fn());

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

当obj在全局声明时,无论obj.c在什么地方调用,这里的this都指向全局对象,而当obj在函数环境中声明时,这个this指向undefined,在非严格模式下,会自动转向全局对象

使用call,apply显示指定this

call与applay后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同

1
2
3
4
5
6
7
8
9
function fn(num1, num2) {
console.log(this.a + num1 + num2);
}
var obj = {
a: 20
}
fn.call(obj, 100, 10); // 130
fn.apply(obj, [20, 10]); // 50

实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义父级的构造函数
var Person = function(name, age) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
}
// 定义子类的构造函数
var Student = function(name, age, high) {
// use call
Person.call(this, name, age);
this.high = high;
}
Student.prototype.message = function() {
console.log('name:'+this.name+', age:'+this.age+', high:'+this.high+', gender:'+this.gender[0]+';');
}
new Student('xiaom', 12, '150cm').message();
// result
// ----------
// name:xiaom, age:12, high:150cm, gender:man;

在Student的构造函数中,借助call方法,将父级的构造函数执行了一次,相当于将Person中的代码,在Sudent中复制了一份,其中的this指向为从Student中new出来的实例对象。call方法保证了this的指向正确,因此就相当于实现了基层。

1
2
3
4
5
6
7
var Student = function(name, age, high) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
// Person.call(this, name, age); 这一句话,相当于上面三句话,因此实现了继承
this.high = high;
}

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

这是面向对象编程的核心要点,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name, age) {
// 这里的this指向了谁?
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
// 这里的this又指向了谁?
return this.name;
}
// 上面的2个this,是同一个吗,他们是否指向了原型对象?
var p1 = new Person('Nick', 20);
p1.getName();

通过new操作符调用构造函数,会经历以下4个阶段。

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

基本上prototype对象指向new操作符生成的实例,这里指向p1。

查阅资料:感谢波同学的分享 csdn的一篇博客


欢迎转载,但转载请注明来自”勿忘初心”的博客,并保持转载后文章内容的完整性。

本文链接:http://lucylc.com/JavaScript/2017/03/02/Js函数作用域与this的指向问题.html

坚持原创技术分享,您的支持将鼓励我继续创作!

热评文章