美文网首页Web前端之路
类与 this:从 C++ 到 JavaScript

类与 this:从 C++ 到 JavaScript

作者: 一半晴天 | 来源:发表于2018-06-03 22:54 被阅读146次

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 代码的风格。 值得说明的是在 Gofunc (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; 的代码。
看起来JavaScriptJava 是有一点相同的,但是涉及到 this 的指向方面,它们之间的差别就天差地别了,估计有 雷锋雷锋塔 的差别那么大。

JavaScript 中与 Javathis 的区别,我认为主要在于, JavaScript 中的 this 是运行时确定的。而 Java 中的 this 的指向在编译期就已经固定了。
另外一个重要的区别是,函数在 JavaScript 是可以像 数字,字符串,实例这样可以赋值通过参数传递的,这也是 JavaScript 很重要的一个特点,我们经常听到的一句话便是:函数是一等公民。 伴随着函数一等公民的身份,JavaScript 拥有闭包的特性。

Java 中的 this 的概念比较简单,我们就略过,重点看看 JavaScript 中的 this 的表现。

首先可以确定的一点是 JavaScript 中的 this 也是指向某个对象的。问题的关键是要确定它指向哪一个对象。

经典 JS 代码中的 this

  1. 元素事件中的 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 中的对象。

  1. 全局作用域中的 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.namethis.age 在定义这个方法的时候显然知道 obj 定义有这两个字段。但是函数是一等公民。存在这样使用的可能性。

const anotherObj = {};
// 在某个地方引用了 obj.showIntro;
anotherObj.showIntro = obj.showIntro;
// 在某个地方调用了。
anotherObj.showIntro();

这个时候 showIntro 方法在运行时 this 指向的是 anotherObj ,而 anotherObj 并没有 nameage 等字段。所以造成 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.
箭头函数还有一些其他的特性,后面再写笔记总结,先让一句总结性的话:箭头函数是一种随用随写,用完即弃的函数。

相关文章

网友评论

    本文标题:类与 this:从 C++ 到 JavaScript

    本文链接:https://www.haomeiwen.com/subject/dfrjsftx.html