所谓的对象,就是抽象化的数据本身

一个面向对象转向面向原型的困惑

我发现Javascript这门语言每次翻开都会带给人新感受,尤其是看完其他语言的面向对象再来看它,但是如果你也是过来人就一定记得教科书里面冗长乏味的面向对象,所有的书上都会跟你这么说:面向对象是对要解决的问题的一种抽象,比如很经典的Java或者C++,class就是根基,然后类实例化出对象balabala....初学者来看的话其实是很难接受的,但是挺过这个时期,就会产生一种理所应当的感觉,就会觉得:面向对象当然需要类啦当然需要实例化啦,不然怎么继承的之类的。当然基本上主流的面向对象语言都会提供基本相同的概念。但是有个异类就是JavaScript,如果你用其他语言的概念来理解这个世界里的对象可能会找不着北。因为这个世界里没有“类”这个东西,所有的东西都是对象。
朝下看的时候,我建议学过面向对象的人忘掉“类”这个东西,不然很容易就会搞混。

什么是对象?

那到底什么是对象呢?这个问题问浅了是个傻问题问深了又变成了一个哲学问题,每个语言甚至每个人都有不同的答案,但是如果你只有十天来设计一门语言的话,你肯定也是想把这个东西设计的越简单越好,所以对JavaScript来说,对象就是属性+方法,简单来说就是这样:

var Person={
    name :"XXX",
    age:18,
    address:"YYY",
    gender:0,
    eat:function(){
        console.log("食");
    },
    wear:function(){
        console.log("衣");
    },
    live:function(){
        console.log("住");
    },
    walk:function(){
        console.log("行");
    }
}
Person.eat();

这样一个对象就写好了,不仅如此,我们在运行时还可以动态的修改内部对象的属性,以及增加方法等等非常自由。第一眼看上去非常的直观,但是仔细想想看,问题其实很多,比如,属性这样无限制的访问一点安全性都没有,再比如我想再要生成一个人但是名字叫小红的,就很费劲,再再比如我怎么实现继承?问题很多我们一个一个来说

纯对象怎么完成继承?

其实继承说白了,要做的事情就是把两个毫不相干的人建立父子关系,但是怎么建立呢?Javascript语言的对象生来都有一个特殊属性叫__proto__,我们可以用这个属性来关联其他对象,就像这样:

var Teacher={
    //这里添加老师的属性和方法
    __proto__:Person
}
Teacher.eat();

这样,两个对象之间就建立了联系,人类(对象)是老师(对象)的原型,用图表示就是:

clipboard.png

这样一条链条把对象之间联系起来,这样使用Teacher调用eat方法的时候找不到就顺着链子朝上找一直找到头如果没有就报错,看起来很完美。

怎么欺骗其他世界的程序员?

但是大家都知道高级语言都是要吸引别人来用的,这个方式实在是和其他语言不太一样,怎么吸引其他人来用呢?本着不行就封装一层的原则于是语言提供了构造函数,但是这个构造函数到这里为止还是和其他语言有完全不同的意义(当然使用上区别不大)。

function Person(){
    this.name=name;
    this.eat=function(){
        console.log(this.name+" eat food now");
    }
}

ming =new Student("xiaoming");
hong =new Student("xiaohong");

这个构造函数的不管从调用方式还是内部写法就都很有Java Class的感觉,但是从用途上来说,它其实更靠近的概念是Java中的工厂方法。而且使用的时候还使用了new这个关键字,同时解决了上面的那个不用生成一个对象就写一大串代码的尴尬。同时还很灵活,你还是可以在ming或者hong这个对象上动态添加方法。

但是上面的操作有个很操蛋的问题就是,因为这个语言没有类只有对象,所以你构造函数里写的方法会原封不动的出现在新生成的对象当中,这意味着每个新生成的对象都有相同的函数,这就很浪费而且对象多了还吃内存。设计者当然也想到了这个问题,他用纯对象的思考方式想了一个解决办法,举个例子:
现在有一个Person构造函数(上面那样的),通常使用它来生成新的对象(小红小明等等)来通过原型链来访问父对象的方法,基于这个模型那我索性就再生成一个对象挂在Person下面,用一个属性指向它(大家都知道我说的就是prototype这个属性啦),公共的方法完全可以都放在这个对象里面,但是怎么调用这些方法呢?其实大家既然都是对象,小明小红的__proto__里面写的只要是Person.prototype就完事了呀,说了这么多用一个图来标识一下:

clipboard.png
对我们来说只需要关注横着一排的原型链就行了,至于Person构造函数是一个函数自然也是一个对象(函数也是对象),同样自然有自己的原型链但是这里和主体无关就不体现在图上了。

如果原型链的知识都差不多了的话我觉得就可以放出下面这张广为人知的图来记忆一番了:

clipboard.png
然后写了这么多,你就发现其实继承在Javascript当中原来也是一个谎言--只要把新的对象挂上原型链就算是“继承”了。那问题就变成了怎么构建原型链,我们还是放上道爷发明的一种方式来实现继承(方法多种多样,我觉得道爷的这种桥接并且不污染上下文环境的方式相当好用)

//父类
function Student(props){
    this.name=props.name||"unnamed";
}
Student.prototype.hello=function(){
    console.log('Hello, ' + this.name + '!');
}
//子类
function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}
//使用一个空构造函数来桥接
function F(){}

F.prototype=Student.prototype; //把原本指向Function.prototype的指针指向父类
PrimaryStudent.prototype=new F();//桥接子类对象到一个F的匿名对象上
PrimaryStudent.prototype.constructor=PrimaryStudent;//纠正构造函数指向

PrimaryStudent.prototype.getGrade=function(){
        return this.grade;
}

// 开始测试
var xiaoming=new PrimaryStudent({
    name:'xiaoming',
    grade:2
});
console.log(xiaoming.__proto__===PrimaryStudent.prototype);
console.log(xiaoming.__proto__.__proto__===Student.prototype);
console.log(xiaoming instanceof PrimaryStudent);
console.log(xiaoming instanceof Student);

使用一张图展示上面的代码做了什么:
clipboard.png

done!关系就是这么桥接好的,我们如果再在外面包一层函数就完全可以做工具函数来用

ES6标准下的语法糖

很明显,上面的这个做法很费劲,或者说,不直观没法吸引别人来用,所以ES6标准里加了一个很Java的语法糖,就像下面这么写:

class Person {
    constructor(name){
        this.name=name;
    }
    hello(){
        console.log(`${this.name} say hello to you!`);
    }
}
class Teacher extends Person{
    constructor(name,gender){
        super(name);
        this.gender=gender;
    }
    myGender(){
        console.log(`${this.name}'s gender is ${this.gender}`);
    }
}

var x=new Teacher("niuguangzhe","nan");
x.myGender();

可以说是Java味道十足,但是别忘了,其实语言的内部仍然是原型链。

到这里,所有关于继承的东西讲完了,接下来准备准备说说Javascript当中的封装


已注销
214 声望5 粉丝

写代码不要局限某种语言,解决问题才是最重要的