阅读 1379

给程序员看的Javascript攻略 - Prototype (上)


原文发表在: holmeshe.me , 本文是汉化重制版。

本系列在 Medium掘金上同步连载。

还记得早先用ajax胡乱做项目的时候踩过好多坑,然后对JS留下了“非常诡异”的印象。最近换了一个工作,工作语言就是JS。然后发现这个语言真不得了,前面后面都能干,基本成了全栈的同义词。所以刚好就趁这个机会系统学习一下这个语言。因为是给程序员看的,这个系列不会讲基本的“if-else”,循环,或者面向对象。相反,我会关注差异,希望能给您在Pull Request里走查代码式的学习体验!

prototype, 仿版OOP

一句话定义:prototype 是JS引擎在运行时,给每个类创建的一个单例模板,该类的对象都通过把这个模板在内存里拷贝一份来创建。prototype用一种非常规的方式实现了OOP,我把它称为仿版表示它不一样,而并没有任何贬低的意思。

打上码,我们先感受下prototype有多不一样:

function inherits(ChildClass, ParentClass) {
  function IntermediateClass() {}
  IntermediateClass.prototype = ParentClass.prototype;
  ChildClass.prototype = new IntermediateClass;
  ChildClass.prototype.constructor = ChildClass;
  return ChildClass;
};复制代码

这是一个用prototype实现继承的示例,现在看不懂没关系,因为我会在下篇详细讨论基于prototype的继承。

学这个有用不?

上篇里面讲道,ES6已经在JS里定义了一套更加标准的OOP机制。而且基于ES6的代码是可以被转译成ES5的语法,所以新标准在应用中可以达到老标准同等的兼容性。这个是说prototype不用学了吗?我认为不一定。ES5仍然在已有的代码中大规模存在着,如果在AngularReactVue的代码库下面运行这段命令:

grep -R prototype *|wc -l复制代码

结果分别是286,405和756处。所以对于我而言,哪怕是以理解这1447行基础代码为目的,也要把prototype当做必修课。尤其要注意,这三个框架我是按字母顺序拍(紧张打错字,排)的先后次序😅。

玩转prototype

在下面的例子中,我们首先定义一个构造函数(如果您不熟悉JS语境下的构造函数,请⬅到我的上一篇文章)然后我们基于这个构造函数创建对象来检查他们的prototype

打上码:

var MyClass = function (){};
var obj = new MyClass;

alert(obj.__proto__ == MyClass.prototype); //=>true
alert(obj.prototype == MyClass.prototype); //=>false
// if the object's prototype does not equal to the class' prototype, what is it then
alert(obj.prototype); //=>undefined

// you can change the definition of a class on the run
MyClass.prototype.a_member = "abc";
// and the object's structure is changed as well
alert(obj.a_member); //=>abc
alert(obj.__proto__.a_member); //=>abc复制代码

运行结果:

true
false
undefined
abc
abc复制代码

初步观察,我们可以得到以下印象:

  1. 只有类(class),或者说构造函数才包含prototype。并且这个prototype可以通过生成对象实例的__proto__访问的到;
  2.  prototype__proto__ 其实是一个硬币的正反面,它们同时指向的一个实体,就是本篇开头所说的那个单例
  3. 如果类的prototype在运行时改变,这个变更会传递给所有实例。

更进一步,如果在运行时改变某个实例的__proto__,这个改变会向上改变类的prototype(类定义),进而会级联更新所有其它同类型实例的结构。

打上码:

var MyClass = function (){};
var obj1 = new MyClass;
var obj2 = new MyClass;
obj1.__proto__.a_member = 'abc';

// an instance can change the definition of the class
alert(MyClass.prototype.a_member); //Ooooops...
// so all other instances are affected
alert(obj2.a_member);复制代码

运行结果:

abcabc复制代码

这是一个作死的操作,因为谁都受不了变来变去的类声明。所以我建议把__proto__想象成常量,别去动就好了。

但是,如果你实在想在运行时动定义,那就动实例本身吧,因为动实例没有潜规则:

var MyClass = function (){};
var obj1 = new MyClass;
var obj2 = new MyClass;

obj1.a_member = 'abc'; // 动对象的定义

// 不打紧
alert(MyClass.prototype.a_member);
// 不打紧
alert(obj2.a_member);复制代码

运行结果:

abc
abc复制代码

从另一方面说,你也可以直接动类的定义,不但没有副作用,还能碰巧让JavaScript支持static成员变量(或函数):

var MyClass = function (){};
var obj1 = new MyClass;
var obj2 = new MyClass;
MyClass.a_member = 'abc'; // 动类的定义

// 不打紧
alert(MyClass.prototype.a_member);

// 不打紧
alert(obj1.a_member);
alert(obj2.a_member);

// the member has been effectively added
alert(MyClass.a_member);复制代码

运行结果:

undefined
undefined
undefined
abc复制代码

所以用好了这也可以是个神操作,主要是由于

一切皆对象

在JavaScript里,一切皆对象。这一切包括,函数,构造函数(类),对象实例,prototype以及__proto__,等等。我们来用码证明一下:

var MyClass = function (){  this.a = "a";  this.b = "b";};
var obj = new MyClass;var arry = [];
function f1() {
  alert("something");
};

alert(MyClass instanceof Object);
alert(MyClass.prototype instanceof Object);
alert(MyClass.__proto__ instanceof Object);
alert(obj instanceof Object);
alert(obj.__proto__ instanceof Object);
alert(obj instanceof Object);
alert(arry instanceof Object);
alert(f1 instanceof Object);
alert(f1.prototype instanceof Object);
alert(f1.__proto__ instanceof Object);复制代码

运行结果:

true(合唱)复制代码

更进一步说,对象可以细分为一等对象普通对象。普通对象就是一般的变量了,它们可以在运行时被创建,被改,被销毁或者被赋值给其它变量。而一等对象则是“柏金版”的普通对象,是解锁了会员特权的。所以,除了“被创建,被改,被销毁或者被赋值给其它变量”以外,它还能被(作为普通函数)调用,还能(作为构造函数)创建实例。这些多出来的特权是不是可以从另一个方面解释为啥要多给一等对象一个prototype呢?你怎么看?

打上码:

var MyClass = function () {
  this.a = "a";
  this.b = "b";
};

var obj = new MyClass;
function f1() {
  alert("something");
};

// MyClass is a first class object so...
alert(MyClass.prototype); // it has prototype and
alert(MyClass.__proto__); // __proto__

// obj is an object so...
alert(obj.prototype);
// it does not have prototype but
alert(obj.__proto__); // it has __proto__

// f1 is a first class object so...
alert(f1.prototype); // it has prototype and
alert(f1.__proto__); // __proto__复制代码

运行结果:

[object Object]
function () {}
undefined
[object Object]
[object Object]
function () {}复制代码

除了。。。

例外一

我刚刚讲了一切皆对象吗?

var str = "whatever";
alert(str instanceof Object);
alert(str.__proto__ instanceof Object);复制代码

运行结果:

false
true复制代码

从上码(没写错)可以看出,尽管字符串常量不是对象,它也有一个__proto__。所以有两种可能性,要么字符串也是对象(明显不是),要么我上面讲的都是错的。

其实没错啦。这里是由于一种叫自动包装的机制起了作用,所以基本数据类型被自动包装成对象,然后调用相应的成员。

打上码:

var str = "whatever";
alert(str.indexOf("tever"));复制代码

运行结果

3复制代码

同理,字符串常量在运行时被自动包装成String()对象,然后调用indexOf()成员函数。这个机制在其它基本数据类型(int, float)上面也有效哦。

例外二

好像很完美的样子,那再试试下面这段码:

var obj = { a:"abc", b:"bcd"};

alert(obj instanceof Object);
alert(obj.__proto__);
alert(obj.__proto__ instanceof Object);复制代码

运行结果:

true
[object Object]
false复制代码

显然,从前两条的结果看 var objobj.__proto__ 都是对象。但是当我想确认一下的时候,obj.__proto__又不是了。我好像成了一个乱写自己都不懂的东西的博主了,但是如果您有答案的话,不妨在下面留言。其实如果没有也不打紧,因为__proto__本来也是非标准用法。

下一篇我会讨论一个难点,基于 prototype 链的继承。

好了,今天先写到这。如果您觉得这篇不错,可以关注我的公众号 - 全栈猎人。


也可以去Medium上随意啪啪啪我的其他文章。感谢阅读!👋


文章分类
前端
文章标签