JavaScript 的面向对象

JavaScript 的面向对象

class Complex {
    constructor(_real, _imaginary) {
        this.real = _real;
        this.imaginary = _imaginary; // Declare members & initialize
    }
  
    print() {
        console.log(`${this.real} ${this.imaginary >= 0 ? "+" : "-"} ${Math.abs(this.imaginary)} i`);
    }

    static printInfo() {
        console.log("This is a complex class.");
    }
}

let com = new Complex(1, 2);
com.print(); // "1 + 2 i"
Complex.printInfo(); // "This is a complex class."

有一些细节需要注意:

  • 类方法内使用类成员的时候必须使用 this 调用
  • JavaScript 的所有类成员和类方法均是公有的,JavaScript 不具有访问权限修饰符
    • 但也并非完全没有办法限制,你可以考虑使用 gettersetter,或使用 Proxy 对象来解决这一问题,这些技巧我们并不作过多展开,感兴趣的同学可以参考这些内容:

this 的指向问题

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

foo(); // window

let obj = { foo: foo, };
obj.foo(); // { foo: foo }

这里两次调用的都是同一个 foo 函数,不同的是前者是全局调用(即直接调用),而后者是把函数作为一个对象 obj 的属性后通过 obj 调用。而 this 的指向就是在函数调用的时候确定的。全局调用 foo 函数,那 this 就指向全局对象,通过 obj 调用,那 this 就指向 obj

let obj = {
    foo: 1,
    inner: {
        foo: 2,
        fn: function () {
            console.log(this.foo);
        },
    },
};

obj.inner.fn(); // 2

obj.fn = obj.inner.fn;
obj.fn(); // 1

let fn = obj.inner.fn;
fn(); // undefined

这里的 fn 函数被三个对象分别调用,从上到下为 obj.inner, obj, window,可以看出这三次调用中的 this 就分别指向这三个对象。

比如说 call(), apply() 方法就允许我们手动指定 this 的指向,其接受的第一个参数就会成为 this 的指向:

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

let obj = { a: 1, };
let another = { a: 2, };

foo(); // undefined
foo.call(obj); // 1
foo.apply(obj); // 1
foo.call(another); // 2
foo.apply(another); // 2

bind() 方法则允许我们将 this 的指向永久绑定于某一个对象上。无论后续使用哪一个对象调用这个函数,其 this 都保持原先 bind() 设定的指向:

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

let obj = { a: 1, };

foo(); // undefined
foo = foo.bind(obj);
foo(); // 1

let another = { foo: foo, a: 2, };
another.foo(); // 1

在这些补丁出现之前,为了硬性规定 this 的指向,程序员甚至会在函数中使用 var that = this 语句先捕获 this,之后使用 that 代替 this 来防止 bug。

而现在更常用的一种解决方法是箭头函数,这种新特性值得专门开出一节来讲讲。

箭头函数

之前我们已经讲过如何使用箭头函数,但并没有讲它除了写法简便以外的实现区别,实际上箭头函数的设计很大程度上是为了解决 this 问题。

前面提到过,this 总是指向最近的调用者,对于一个 function,在内部使用 this 时,指向的是函数调用时的上下文;但对于箭头函数,其本身并没有 this,也即箭头函数的 this 实际上是定义箭头函数时从上下文中获取的,箭头函数作为一个闭包,保存了定义它时的 this,这样就解决了 this 指向不明确的问题。

箭头函数只是函数的另外一种写法:

const sum = (x, y) => {
    return x + y;
}

sum(1, 2); // 3

同时,对于一些函数体没有中间操作的函数,可以直接在箭头右侧写返回值:

const sum = (x, y) => x + y;
sum(1, 2); // 3

而在回调函数里面写箭头函数将会让代码更可读:

response.data
    .map((val, ind) => ({
        ...val,
        index: ind,
    }))
    .filter((val) => val.name !== undefined)
    .sort((a, b) => a.subscriberNum - b.subscriberNum);

唯一要注意的是,如果一个箭头函数直接返回一个对象,这个对象的花括号可能会和代码块的花括号混淆,所以如果直接返回一个对象,记得在对象的花括号外再套一层小括号防止出现 Syntax Error。

箭头函数和 function 函数最大不同的地方是其 this 的指向跟随上下文的 this,也就是说其本身不具有 this,在箭头函数体里使用的 this 实际上是其上下文(一般是其所在的函数作用域)的 this。换句话说,箭头函数的 this 在定义的时候就完成了绑定,这样的性质就解决了 this 指向不明的问题。

考虑这样的代码:

const foo = () => console.log(this);

foo(); // window

// Changing caller will not change 'this'
let obj = { foo: foo, };
obj.foo(); // window

// 'call' & 'bind' may fail
foo.call(obj); // window
foo = foo.bind(obj);
foo(); // window

可以看出,在全局环境下设定的箭头函数的 this 永远跟随全局环境指向全局对象。

但另外一方面:

let obj = {
    bar: function () {
        const foo = () => console.log(this);
        return foo;
    },
};

const foo = obj.bar();
foo(); // obj

const bar = obj.bar;
bar()(); // window

这里前面获取 foo 的时候是使用的 obj 调用的普通函数 bar,所以 barthis 指向 obj,跟随着的 foothis 就指向 obj。而后面获取的时候 bar 是被全局对象调用的,其 this 就被自然绑定到了全局对象上,所以内部箭头函数的 this 也就绑定到了全局对象上。