this 是什么?
首先我们先把目光投向一种古典的编程语言 C with Classes
也就是 C++
的前身。
一开始映入我们眼帘的是一个 stack
类。
class stack{
char s[10];
char *min;
char *top;
char *max;
public:
void push(char c){
if(top > max)error("stack overflow")
*top++ = c;
}
}
void f1(class stack *p){
p->push('c');
}
上面的 C++
代码会生成如下的 C
代码。
struct stack{
char s[10];
char *min;
char *top;
char *max;
}
void stack__push(struct stack * this, char c){
if( ( this ->top ) > ( this ->max ) ) error("stack overflow");
*(this->top)++ = c;
}
void f1(struct stack *p){
stack__push(p, 'c');
}
c++ 中的结构(类)的字段的 布局与 c 中结构的内存布局是相容的。
在 C with Classes
方法翻译成 C
语言时。 this
就是指向其实例对象的一个指针。从 C 语言的角度来看, 类函数的封装及 this
是语法糖,完全是可以使用结构加函数模拟出 C with Classes
类的效果。 下面我们这里重点是关注 this
指针的语法糖。
上面说到结构对象指针 this
的语法糖。 如果没有这个语法糖。
那可能我们写类的时候就都得这样写.
// 声明
class stack{
public:
void push(stack *this, char * c);
}
// 如果按一般做法将声明与实现分开
void stack::push(stack *this, char * c){
}
// 使用时的两种可能写法。
stack *myStack = new stack();
// 可能的写法 1)
myStack->push(myStack,'a');
// 可能的写法 2)
myStack->push('a');
如果在声明时没有 this
的语法糖,那么使用时的的写法2,看起来有点参数对不上。如果使用写法 1) 看上去写法有点冗长。
C with Classes
对于此提供了 this
语法糖的支持。
也就是只要这样写就可以了。
class stack{
public:
void push(char * c);
}
stack::push(char *c){
}
myStack->push('a')
看起来清爽明了。
既然已经讲到了this
语法糖这种写法,我突然想到。两种语言,一种是 Python
,别一种是 Go
。 它们在类的方法声明时不像 C++
一样在声明时有提供隐式的 this
对象指针的语法糖。而是需要显示声明。
首先来看一下,具有强类型的语言新秀 Go
的写法。
// 声明一个新的类型 `stack` ,它的数据类型是 `struct`
type stack struct {
chars []byte
}
// 为 `stack` 对象 定义一个 `Push` 方法。
func (this *stack) Push(char byte) {
this.chars = append(this.chars, char)
}
// 使用
myStack := &stack{}
myStack.Push('a')
你会发现 Go
语言的实例方法的风格就很像,C with Classes
的实例方法翻译出来的 C 代码的风格。 值得说明的是在 Go
中 func (this *stack) Push(char byte){}
中的 this
参数名可以是任意有效的参数名。这个随意,这个是由于没有了 this
隐式实例引用指针的语法糖的写法上的一种自由。
对于 Go
来说虽然没有 C++
的 this
语法糖,但是使用上一般比 C++
简洁一些。 因为 C++
中一般是先在类中声明了方法。实现单独出来定义。再加上单独定义时方法名前加类名限定,比如 stack::push(char c)
实际写法上并不比 Go
省多少。
再看看 Python
的写法。
# 声明 stack 类
class stack(object):
def __init__(self):
self.chars = []
def push(self, char):
self.chars.append(char)
# 使用
myStack = stack()
myStack.push('a')
Python
中的实例方法没有 this
语法糖。实例方法的第一个参数就 this
只不过使用的时候并不需要这样冗长的写法: myStack.push(myStack,'a')
。
值得说明是,虽然语言层面上没有限制第一个参数必须是 self
。但是如果不写成 self
是很让人感到奇怪的。
JavaScript 中的 this
使用 C++
,Go
, Java
,Python
这些语言的人,基本都知道 this
(或self
)指的是什么,也基本没有犯糊涂的时候。但是写 JavaScript
的同学,比如我,经常会思考这里的 this
指的是什么?有时也会写不会像 let that = this;
的代码。
看起来JavaScript
跟 Java
是有一点相同的,但是涉及到 this
的指向方面,它们之间的差别就天差地别了,估计有 雷锋 和 雷锋塔 的差别那么大。
JavaScript
中与 Java
中 this
的区别,我认为主要在于, JavaScript
中的 this
是运行时确定的。而 Java 中的 this
的指向在编译期就已经固定了。
另外一个重要的区别是,函数在 JavaScript
是可以像 数字,字符串,实例这样可以赋值通过参数传递的,这也是 JavaScript
很重要的一个特点,我们经常听到的一句话便是:函数是一等公民
。 伴随着函数一等公民的身份,JavaScript
拥有闭包的特性。
Java
中的 this
的概念比较简单,我们就略过,重点看看 JavaScript
中的 this
的表现。
首先可以确定的一点是 JavaScript
中的 this
也是指向某个对象的。问题的关键是要确定它指向哪一个对象。
经典 JS 代码中的 this
- 元素事件中的
this
比如下面一行代码 <button onclick="alert(this.toString())"> 点我试试</button>
onclick
事件属性对应的代码中 alert(this.toString())
中的 this
指向的是什么呢?对于没有 JavaScript
编程经验的人,这是很难一眼看出来的。
首先对于给 button
元素设置 onclick
属性的背后发生了什么呢?
我猜测是这样子的。
button.onclick = function(){
alert(this.toString());
}
我们看一眼 Chrome 给出的答案:
> document.querySelector("button").onclick
ƒ onclick(event) {
alert(this.toString())
}
当点击按钮之后你会发现 this.toString()
的弹窗是 [object HTMLButtonElement]
也就是说 this
指向的就是 button
这个元素对应在 DOM 中的对象。
- 全局作用域中的
this
比如我们script
元素内的 代码:
<script>
alert(this.toString());
function showThis() {
alert(this.toString());
}
showThis();
</script>
运行之后发现上面两个 this.toString()
返回的是 [object Window]
, 一般来说浏览器中的全局对象对应就是 window
这是规定。
this 的运行期绑定特性。
上面的例子中 showThis
函数中的 this.toString()
返回的是 [object Window]
. 我们将代码修改一下,使用一下函数一等公民的特性。
<script>
function showThis() {
alert(this.toString());
}
const obj = new Object();
obj.showThis = showThis;
obj.showThis();
</script>
此时再运行时 this.toString()
返回 [object Object]
也就是说当我们通过 obj
调用其showThis
属性指向的函数时函数内的 this
就指向 obj
对象。 这就是 this
的运行期绑定特性。
如果应对漂移的 this ?
比如说我们有一段代码,如下:
const obj = {
name: "banxi",
age: 18,
showIntro() {
const intro = `Hi, my name is ${this.name}, I'm ${this.age}`
alert(intro);
}
};
obj.showIntro();
在 showIntro
方法中,引用了两个 this
对象的字段。this.name
和 this.age
在定义这个方法的时候显然知道 obj
定义有这两个字段。但是函数是一等公民。存在这样使用的可能性。
const anotherObj = {};
// 在某个地方引用了 obj.showIntro;
anotherObj.showIntro = obj.showIntro;
// 在某个地方调用了。
anotherObj.showIntro();
这个时候 showIntro
方法在运行时 this
指向的是 anotherObj
,而 anotherObj
并没有 name
和 age
等字段。所以造成 showIntro
运行异常?
避免这样问题,对于一般的代码,我们可以尽量避免将实例方法赋值给其他的对象。
但是很多时候也是无法避免的,而且问题看起来比上面的代码隐蔽多了。比如下面的代码:
let button = document.querySelector("button");
class ViewModel {
constructor(button) {
this.button = button
button.onclick = this.onTapButton
}
onTapButton(e) {
alert(this.toString())
}
toString() {
return "[object ViewModel]"
}
}
new ViewModel(button);
看起来很自然的把属性 ViewModel
类的一个实例方法赋值给了 button
对象。
而在 onTapButton
中写代码的时候以很容易的认识 当前的 this
指向的是 ViewModel
类的对象。实际运行时, 根据我们上面的分析不难得知 this
指向的是 button
对象。
但是,我们很可能想让 onTapButton
方法中的 this
指向 ViewModel
的对象。因为不指向 ViewModel
对象的话,我们无法调用其他实例方法,而且也比较违反我们在此方法中写代码的直觉。
针对这种问题 JavaScript
提供了 bind
方法。也就是说我们可以将某一个函数绑定到某一个对象中,不随赋值给不同的对象而转移。
这样写:
this.onTapButton = this.onTapButton.bind(this)
button.onclick = this.onTapButton
这样调用bind
之后,后面就无法再绑定到其他对象上去了。也就是说第二次调用 bind
基本是无效的。bind
的原理就是通过闭包来锁定第一次绑定的对象。 然后再经过 apply
在调用时指定此绑定的对象为 this
参数。此实现原理写代码代码就是下面的这个样子:
// 注意:此实现仅作演示实现原理,不应该用于生产环境。
Function.prototype.myBind = function (toObj) {
const oldFun = this;
return function () {
oldFun.apply(toObj, arguments)
}
}
// 使用
this.onTapButton = this.onTapButton.myBind(this)
对于上面的这种问题,在 ES 6 中,我们还有另一种解决办法。就是下面要介绍的 ,箭头函数。
箭头函数
我们将上面经典代码中的 onclick
使用箭头函数来改写。
<script>
let button = document.querySelector("button");
button.onclick = (event) => {
alert("this is? " + this.toString())
}
</script>
想想,this
指向什么对象。 Chrome 给我们的答案是 [object Window]
。
也就是全局对象。
我们再把上面的代码使用 ES 6的类风格改写一个。
let button = document.querySelector("button");
class ViewModel {
constructor(button) {
this.button = button
button.onclick = (event) => {
this.onTapButton()
}
}
onTapButton(e) {
alert(this.toString())
}
toString() {
return "[object ViewModel]"
}
}
new ViewModel(button);
此时 this.toString()
返回的是 [object ViewModel]
。
对此我们可以得到一个关于箭头函数的 this
指向的结论。即箭头函数的 this
指向的是其最近的外围的 this
.
箭头函数还有一些其他的特性,后面再写笔记总结,先让一句总结性的话:箭头函数是一种随用随写,用完即弃的函数。
网友评论