Javascript中的原型链分析

0x00 前言

Javascript中的prototype是一个十分重要的概念,但是网上的教程一般分析得比较绕,结果越看越晕,反而变得更加难以理解了。

本文尝试由浅入深,从实验入手,来深入地理解这一概念。

0x01 函数与对象

函数是JS中最为重要的一个概念,下面是创建函数最简单的方法:

  1. function func(){
  2. return 0;
  3. }
COPY

通过Chrome开发者工具,可以得到以下输出:

  1. > typeof func
  2. < "function"
  3. > func instanceof Function
  4. < true
  5. > func instanceof Object
  6. < true
COPY

可以看出,funcFunction的一个实例,同时也是Object的一个实例。这点可以理解成Function本质上也是Object的一种。

  1. > typeof Function
  2. < "function"
  3. > typeof Object
  4. < "function"
COPY

再来看这段输出,按照通常OOP语言的理解,FunctionObject的类型应该是class之类的值,但偏偏这里返回的是function。这是为什么呢?

我们知道,js中class的概念是在ES6中才出现的,可以通过以下代码创建一个class

  1. class MyClass {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. show(){
  6. console.log(this.name);
  7. }
  8. }
  9. var obj = new MyClass('drunkdream');
  10. obj.show();
COPY

现在来测试一下obj实例的相关情况:

  1. > typeof obj
  2. < "object"
  3. > obj instanceof MyClass
  4. < true
  5. > obj instanceof Object
  6. < true
  7. > typeof MyClass
  8. < "function"
  9. > MyClass instanceof Function
  10. < true
COPY

可以看出,obj的确是MyClass的一个实例。但是,奇怪的是:MyClass的类型竟然是function,这点和其它语言的确不太一样。

这是因为:

js中并没有真正的class的概念,class仅仅是function的一种语法糖而已。

来看下在ES5中一般怎么构造一个class的。

  1. function MyClass(name) {
  2. this.name = name;
  3. }
  4. MyClass.prototype.show = function () {
  5. console.log(this.name);
  6. }
COPY

这种写法可以实现和上面那段代码相同的功能,但是很明显,MyClass真的是一个function。也就是说:new一个function得到的其实是一个对象。这和其它语言差异是比较大的。

prototype在其中就是扮演了添加类的成员函数的作用。

其实,将上面的代码改成:

  1. function MyClass(name) {
  2. this.name = name;
  3. this.show = function () {
  4. console.log(this.name);
  5. }
  6. }
COPY

这样的形式对于使用者也是完全没有问题的,差别只是每次实例化都会创建出一个show函数,显然这种写法是不好的。

0x02 prototype与proto

prototype到底是个什么样的存在呢?

  1. > MyClass.prototype
  2. < {show: ƒ, constructor: ƒ}
  3. show: ƒ ()
  4. constructor: ƒ MyClass(name)
  5. __proto__: Object
  6. > typeof MyClass.prototype
  7. < "object"
  8. > MyClass.prototype.constructor === MyClass
  9. < true
  10. > MyClass.prototype.__proto__
  11. < {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
  12. constructor: ƒ Object()
  13. hasOwnProperty: ƒ hasOwnProperty()
  14. isPrototypeOf: ƒ isPrototypeOf()
  15. propertyIsEnumerable: ƒ propertyIsEnumerable()
  16. toLocaleString: ƒ toLocaleString()
  17. toString: ƒ toString()
  18. valueOf: ƒ valueOf()
  19. __defineGetter__: ƒ __defineGetter__()
  20. __defineSetter__: ƒ __defineSetter__()
  21. __lookupGetter__: ƒ __lookupGetter__()
  22. __lookupSetter__: ƒ __lookupSetter__()
  23. get __proto__: ƒ __proto__()
  24. set __proto__: ƒ __proto__()
COPY

上面这段看起来有点绕,需要仔细思索一下。

可以看出,prototype本质上是一个对象,必须要包含constructor构造函数和__proto__对象。

constructor其实就是MyClass函数本身,而__proto__对象看起来就有些神秘了。不过从__proto__.constructor可以看出,它其实就是Object。是不是觉得__proto__指向的是当前类的基类呢?

我们再来测试一下:

  1. > class MyClass1 extends String{}
  2. > MyClass1.prototype.__proto__.constructor == String
  3. < true
COPY

看来的确是这样的,只不过由于js中的类本质上都是function,而每个function都有一个原型,通过这种方式将原型链接起来,就起到了类继承的作用。

0x03 将对象变成函数

下面是网上找的一段代码:

  1. function classcallable(cls) {
  2. /*
  3. * Replicate the __call__ magic method of python and let class instances
  4. * be callable.
  5. */
  6. var new_cls = function () {
  7. var obj = Object.create(cls.prototype);
  8. // create callable
  9. // we use func.__call__ because call might be defined in
  10. // init which hasn't been called yet.
  11. var func = function () {
  12. return func.__call__.apply(func, arguments);
  13. };
  14. func.__proto__ = obj;
  15. // apply init late so it is bound to func and not cls
  16. cls.apply(func, arguments);
  17. return func;
  18. }
  19. new_cls.prototype = cls.prototype;
  20. return new_cls
  21. }
COPY

它可以将一个类实例类型从object变成function

  1. var s = new String();
  2. console.log(typeof s);
  3. var s = new classcallable(String)();
  4. console.log(typeof s);
COPY

输出结果为:

  1. object
  2. function
COPY

也就是说,使用classcallable之后创建的对象,可以当做函数来调用。我们分析一下这里面的原因。

在js中是允许在类的构造函数中返回一个function的,可以使用以下代码进行测试:

  1. function MyClass(flag){
  2. var func = function(){
  3. console.log("call func");
  4. }
  5. if(flag === 1)
  6. return func;
  7. else
  8. return 0;
  9. }
  10. console.log(typeof new MyClass(0));
  11. console.log(typeof new MyClass(1));
COPY

输出结果为:

  1. object
  2. function
COPY

因此,只要修改构造函数的返回值,就可以改变创建出的实例类型,这里正是用了这种方法。

分享

Gitalking ...