`

Javascript学习笔记 What's the "new"?

阅读更多

本文主要记录在学习Javascript(为了减少打字的工作量和读的顺口,以后会使用JS)的过程中,对于“var o = new Obj(1);”这行代码的理解。

在学习和使用JS之前,我学习过一些经典的编程语言,如Java、PHP和C,我可以熟练的使用Java,能够阅读和简单地编写C和C++的代码。

但是当我带着这些经验来接触JS的时候,我发现事情完全变化了,我在很长一段时间内只能用JS实现页面的功能,但是对于类以及类的实例化完全摸不到头脑。

我想这种错误产生于两个原因:
对于JS的学习,我是边工作边学习的,这样学的好处是上手快,缺点也同样显著:基础知识一塌糊涂;
很多JS的知识来源于互联网,网上有很多人自己也是一知半解(当然也包括本文和本文的作者),我看完之后再加上自己的理解,于是我距离真相越来越远了。
言归正传,要理解var o = new Obj(1);,我们首先要知道这条语句是干嘛的?

有过面向对象学习经验的人一定了解,这条语句是类的实例化。

一、要有类和实例化的知识

那么,什么是类,什么又是实例化?

维基百科给了一个比较抽象的定义:类的更严格的定义是由某种特定的元数据所组成的内聚的包。它描述了一些对象的行为规则,而这些对象就被称为该类的实例。

说的具体一些,类就是一些有相同个体的特征抽象,而逆着抽象,从而建立类到个体的过程叫做实例化。

比如中国人就可以作为一个类,每个人中国人都有一个中文名字,这就是类的一个属性。中国人的名字是什么,我们是说不出来的,但作为一个中国人的实例,我的名字是“山谷”。

这是很浅显的知识,尤其是对于计算机知识扎实的人。

二、JS中如何创造类

那么我们来看一段JS代码,JS是如何建立一个类。

var Obj = function(x) {
    this.x = x;
    console.log(this.x);
    //xxxxxxx
};

这是很标准的写法(包括缩进为4个空格而不是一个tab,每行结尾处都没有空格),的确,在JS中,一个Obj的类,就诞生了。

对于没接触过编程的小白还好办,我就这么记忆了,但是有Java或者C++书写经历的人一定会疯掉的,这是函数的声明方法,是的,在JavaScript 2.0之前,JS里面没有“类”(此话是David Flanagan在《JavaScript权威声明》中说的,我特此发表免责声明,由于他在好几个版本都保留了这句话,我假定这句话是真的)。

JS只有“伪类”,没有任何一个JS的结构是类(我们有对象,我们有数组,我们还有函数,我们就是没有类),但是JS可以模仿类,即类有什么,我们就有什么。

那么,类有什么?

类有属性(public、protected、private都行),类还有方法(不管静态的还是动态的),一般情况下,类还有构造函数。

仅以Java为例,我们写一个类看看吧。

public class Obj {
    public int i = 0;//属性
    private int x = 1;//属性

    public Obj(x) { //构造函数
        this.x = x;
    }

    public int getX() { //方法
        return this.x;
    }
}

那么JavaScript都可以模仿。

首先说JavaScript的构造函数,就是Obj这个function(用英文方便上下文对照)。

那么属性和方法可以怎么办,我们会尝试Obj.xxx这个方式,但事实证明,当var o = new Obj(),o是无法调用xxx的。

带着问题(问题是方法和属性挂载的位置),我们开始实例化吧!

三、实例化总共分三步(和把大象装到冰箱里的步骤一样多)

第一步:var o = {};
第二步:o.__proto__ = Obj.prtotype;//Obj.prototype?我们并没有声明prototype,对,这是浏览器自动加入的。
第三步:Obj.call(o, 1, 2);

总结为:第一步,创建一个空对象并赋值给o;第二步是定义原型链使得o指向Obj的原型对象;第三部为用对象o调用Obj函数,调用参数为1和2。

第一步好理解,既然是实例化,就要实例化类为对象,则o必然是一个对象,先赋值成对象,再进行操作。
第二步的难点在于什么是原型链,为什么原型链要o指向Obj的原型对象,那么Obj的原型对象又是什么?
第三步相对于第二步好理解,唯一的一点是call究竟干了什么?

3.1 原型链
根据ECMAScript的解释:每个由构造器创建的对象,都有一个隐式引用 ( 叫做对象的原型 ) 链接到构造器的“prototype”属性值。再者,原型可能有一个非空 (non-null) 隐式引用链接到它自己的原型,以此类推,这叫做原型链。
o.__proto__指向的是o原型链的上级,而其上级的__proto__由指向上级的上级,当使用对象o的一个方法时,如o.xx,js的解析器会沿着o的原型链一直向上寻找,找到最近的一个xx方法,然后调用。
通俗讲,就是通过这个方式,o可以调用Obj原型上的方法。
值得强调的是,o.__proto__并不具有通用性,目前Chrome、Firefox和Safari支持这一属性。

3.2 原型对象
原型对象也可以叫做原型。ECMAScript的定义很简单: 为其他对象提供共享属性的对象。
就JS的类来讲,原型可以理解为挂载变量和方法的容器。
譬如,我们希望在Obj类中添加getS的方法,常规写法是:
Obj.prototype.getS = function() {
    //xxxxxxxxxxx;
};
但是为了方便长时间使用Java和C++的人理解,我们也可以写为:
Obj.prototype = {
    getS: function() {
        //xxxxxxxxxxxxxxxxxxx;
    }
}

3.3 o.__proto__ = Obj.prototype
理解了2.1和2.2,我们就明白了o.__proto__ = Obj.prototype是干嘛的了。
第二步是使Obj上的方法和变量可以为o所用。

3.4 第三步的重点: Obj.call(o, 1, 2)
如果有面向对象的知识的话,可以推出知道,这一步要执行Obj的构造函数。
的确,这一步是由call方法完成的。

那么,call是做什么的?
若有A.call(B, C),则是对象B使用参数C执行A方法,如果你这么回答,似乎是正确的。但我们要向更深处走一步:在执行完call之后,对于A、B、C分别有什么影响。

首先很好理解的是A和C,A使用了C参数,执行了自己,那为什么不直接写A(C),就是为了酷吗?
答案时否定的,若A中有关于this的修改,则this不再指向A原来所在的域,而是指向B。

如下所示:
var A = function(C) { C++; console.log(C); };
var B = {D: 2};
var C = 1;
var D = 1;
这时A.call(B, C);和A(C);是没有区别的。
但是如果A的实现如下所示。
var A = function() { console.log(this.D + C); }
A.call(B, C)的结果就是输出3,而A(C)的结果就是2。

理解了call,我们就能明白Obj.call(o, 1, 2)是做什么的了。
函数Obj(也是类Obj的构造函数),得到了两个输入1和2,再将这两个值赋值给this.x和this.y,最后,将this指向o。

可以思考的问题是为什么属性和方法要挂接到Obj.prototype而不是Obj上?

以上是一些非常肤浅的理解,要解答原型链和原型的很多问题,是需要了解JS的发展历史和浏览器的内核实现,但就工程本身,理解到这里应该可以应付大部分关于类和声明开发了(当然你还有JS的语法知识和算法知识)。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics