最近两周面试了将近30个人,其中绝大部分都是有3年以上的前端工作经验,但是很多人对js的原型和继承的概念都搞不懂,更不用说更深层次的问题了。这也是我写这篇文章的原因吧,同时也是有必要自己做一个知识总结。

Javascript里的类

ES6为我们带来了定义类的keyword class,但这只不过是给程序员少些几行代码的语法糖而已,原型还是必须要掌握的知识。

类从理论上来说就是 数据 + 方法 的集合,那么可以很简单地写出下面的代码

function Person(name) {
    return {
        name: name,
        say() {
            console.log(`Hello, I'm ${this.name}`)
        }
    }
}

let person = Person('jolyne');

写完后就会发现这种“类”很难实现继承,这时就可以借助原型链来实现。

原型链

原型链特性主要有两点:

一是可以通过对象上的原型不断往上查找属性,数组、字符串等对象携带的方法就是通过这种方式实现的。

let a = {};
// 操作对象的原型也可以通过Object.getPrototypeOf和Object.setPrototypeOf
a.__proto__ = {
    __proto__: {
        __proto__: { value: 1 } 
    } 
}

// 这里会一直往上查找__proto__
a.value === 1;

还有一点就是 new 这个keyword会创建 __proto__

function A() {}
let a = new A();
// __proto__会引用构造函数的prototype
a.__proto__ === A.prototype;

// A.prototype === Object.prototype
a.__proto__.__proto__ === Object.prototype;

顺带一提instanceof就是通过比较原型来判断的

[] instanceof Object
// ==>
[].__proto__.__proto__ === Object.prototype

另外还有一个比较有意思的点

function foo() {}
// 函数的__proto__指向了一个内部实现
foo.__proto__ // => native code
// 不过依旧是继承自Object
foo.__proto__.__proto__ === Object.prototype

原型的继承

通过原型链的特性就能得到实现继承的基本思路了

// base
function A() {}
A.prototype.foo = function() {}

// child
function B() {}
// B.prototype = { __proto__: A.prototype };
B.prototype = Object.create(A.prototype);
// B.prototype因为会被修改,所以需要是一个新的对象
B.prototype.bar = function() {}

let b = new B();
b.foo === A.prototype.foo;

这样就实现了基于原型的继承,通常我们会将方法挂在原型上,因为它的定义是同个类下所有对象共享的而且通常不受外部影响。那么对于每个对象上特有的数据要怎么办呢?

最简单的方式就是在子类的构造函数里调用父类

// base
function A(value) {
    this.a = value;
}

// child
function B(value) {
    A.call(this, value);
    this.b = value;
}

静态变量继承

静态变量同样可以通过原型链的方式继承,因为静态变量是绑定在构造函数上的,只需将构造函数加入到原型链中就能查找

function A() {}
A.staticField = function() {}

function B() {}
B.__proto__ = A;
B.staticField === A.staticField;

总结

基本原理就是这么些了,看完这篇你也就能看懂Typescript和Babel如何对类做转换了,有些细节我没讲到,比如容错检查、constructor设置。

Reference