关于javascript:深入理解this

29次阅读

共计 3774 个字符,预计需要花费 10 分钟才能阅读完成。

什么是作用域?

简直所有编程语言最根本的性能之一,就是可能贮存变量当中的值,并且能在之后对这个 值进行拜访或批改。事实上,正是这种贮存和拜访变量的值的能力将状态带给了程序。
若没有了状态这个概念,程序尽管也可能执行一些简略的工作,但它会受到高度限制,做 不到十分乏味。
然而将变量引入程序会引起几个很有意思的问题,这些变量住在 哪里?换句话说,它们贮存在哪里?最重要的是,程序须要时如何找到它们?
这些问题阐明须要一套设计良好的规定来存储变量,并且之后能够不便地找到这些变量。这套规定被称为作用域。
简而言之,作用域是依据名称查找变量的一套规定。
(以上摘录自《你不晓得的 JS》)

什么是 this?

JavaScript 有两种作用域:词法 (动态) 作用域和动静作用域 (即 this)。
词法作用域即函数和变量的作用域在定义的时候就确定了(即申明的地位)。如:

var value = 1;

function foo() {console.log(value);
}

function bar() {
    var value = 2;
    foo();}

bar();

// 后果是 ???

答案是 1. foo 定义在全局对象,所以作用域即 window, foo 外部没有找到 value,就往作用域链下面找,即 window,window 对象有申明 value,则打印 1.
注:浏览器的全局对象默认是 window

在看一个例子(摘取《你不晓得的 JS》):

function identify() {return this.name.toUpperCase(); 
} 
 
function speak() {var greeting = "Hello, I'm " + identify.call( this);    
console.log(greeting); 
} 
 
var me = {name: "Kyle"}; 
 
var you = {name: "Reader"}; 
 
identify.call(me); // KYLE 
identify.call(you); // READER 
 
speak.call(me); // Hello, 我是 KYLE 
speak.call(you); // Hello, 我是 READER

这段代码能够在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(),不必针对每个对象编写不同版本的函数。
如果不应用 this,那就须要给 identify() 和 speak() 显式传入一个上下文对象(context)。

function identify(context) {return context.name.toUpperCase(); 
} 
 
function speak(context) {var greeting = "Hello, I'm " + identify( context);
    console.log(greeting); 
}
var me = {name: "Kyle"}; 
 
var you = {name: "Reader"}; 

identify(you); // READER 
speak(me); //Hello, I'm KYLE

随着你的应用模式越来越简单,显式传递上下文对象会让代码变得越来越凌乱,应用 this 提供了一种更优雅的形式来隐式“传递”一个对象援用。也就是动静作用域。this指的是函数运行时所在的环境(上下文对象)。

理解了作用域和 this,接下来进入本文的主题:

如何确定函数的运行环境?

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

var foo = obj.foo;
var bar = 2;

obj.foo() // 1 
foo() // 2

同样都是 foo 函数,为什么后果不一样呢?这种差别的起因,就在于函数体外部应用了 this 关键字。很多教科书会通知你,this指的是函数运行时所在的环境。对于 obj.foo() 来说,foo运行在 obj 环境,所以 this 指向 obj;对于foo() 来说,foo运行在全局环境,所以 this 指向全局环境。所以,两者的运行后果不一样。

这种解释没错,然而教科书往往不通知你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么 obj.foo() 就是在 obj 环境执行,而一旦 var foo = obj.foofoo() 就变成在全局环境执行?

这篇文章从内存的数据结构以及函数如何存储办法进行了解释。
上面简要总结下:
JavaScript 中函数也是对象。下面代码中 foo 函数有一块独立的内存来保留函数,obj.foo 保留的是这块内存的地址。因为函数是一个独自的值,所以它能够在不同的环境(上下文)执行 (即被任意对象调用)
比方 obj.foo() foo 是在 obj 外部的指向函数的属性,并通过 obj.foo 间接援用了函数,所以函数调用地位上下文对象是 obj,所以,this 指代的是 foo 函数的调用者 obj,打印 1
foo() 是间接应用不带任何润饰的函数援用进行调用, 相当于独立调用,而非由其余对象所调用,这种状况在非严格环境下,this 会默认绑定到全局对象 window, 所以打印 2
注:严格环境下会是 undefined

上面再来看几个例子:
例一:

通过上述解释,funs 通过 obj.funs()调用,所以 funs 内 this 是 obj,尽管 fun2 定义在 funs 外部,然而下面说过:this指的是函数 运行时 所在的环境 (上下文对象)。 和定义在哪里无关。funs() 通过函数援用间接调用,所以会应用默认绑定到 window
上述例子咱们晓得了 this 绑定的两种状况:

  1. 隐式绑定:由指定对象间接调用函数,绑定到指定对象
  2. 默认绑定:非严格模式下 window,严格模式下 undefined

那么如何让例一中 fun2()的 this 也绑定到 obj 呢?
革新下:

let obj = {funs() {let fun2 = function() {console.log(this) } //obj
        fun2.call(this)
    }
}
obj.funs()

通过 call, apply, bind 等办法能够显示指定 this 绑定到哪个对象。这便是 this 的显示绑定。

new

当咱们应用 new 来调用函数的时候,此时函数被当作构造函数来解决,具体 new 的实现能够参考这篇文章,构造函数内的 this 绑定到新生成的对象。

function foo(a) {this.a = a;}  
 
var bar = new foo(2); 
console.log(bar.a); // 2

this 绑定的四种规定

至此,上述内容能够总结出四种规定来确定 this 的值:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。var bar = obj1.foo()
  4. 如果都不是的话,应用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。var bar = foo()

以上出自《你不晓得的 JS》

绑定例外

在某些场景下 this 的绑定行为会出其不意,你认为该当利用其余绑定规定时,实际上利用 的可能是默认绑定规定。

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

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被疏忽,理论利用的是默认绑定规定。

var value = 1;

var foo = {
  value: 2,
  bar: function () {return this.value;}
}

// 示例 1
console.log(foo.bar()); // 2
// 示例 2
console.log((foo.bar)()); // 2
// 示例 3
console.log((foo.bar = foo.bar)()); // 1
// 示例 4
console.log((false || foo.bar)()); // 1
// 示例 5
console.log((foo.bar, foo.bar)()); // 1

另一个须要留神的是,你有可能(无意或者无心地)创立一个函数的“间接援用”,在这 种状况下,调用这个函数会利用默认绑定规定。
如示例 3 赋值表达式 foo.bar = foo.bar 的返回值是指标函数的援用,函数理论是间接应用不带任何润饰的 (匿名) 函数援用进行调用 , 相似 与通过 foo() 调用 (注:这里若调用 foo() 会报 not function 谬误哦,因为全局未声明) 而不是 foo.bar() 调用。依据咱们之前说过的,这里会利用默认绑定。示例 4,5 别离应用 逻辑运算和逗号运算符返回了指标函数的援用,因而后果同示例 3。

留神:对于默认绑定来说,决定 this 绑定对象的并不是调用地位是否处于严格模式,而是 函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。

参考起源

书籍《你不晓得的 JavaScript》
JavaScript 的 this 原理 — 阮一峰
JavaScript 深刻之从 ECMAScript 标准解读 this — 冴羽
JavaScript 深刻之 new 的模仿实现 — 冴羽

正文完
 0