JavaScript | 面向对象编程

Reference : JavaScript教程 - 廖雪峰的官方网站

JavaScript哲学:万物皆对象

JavaScript没有类 (class) 或实例 (instance) 的概念。JavaScript实现面向对象编程的工具是原型 (prototype)。

原型 prototype

以下内容展示JavaScript面向对象编程的原理。

我们定义一个对象robot

var robot = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log (this.name + 'is running ...');
    }
};

我们将这个对象作为模板对象,为了方便理解,重新用Student命名它。

var Student = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log (this.name + 'is running ...');
    }
};

现在想要创建对象xiaoming,同时让这个新的对象获得Student对象相同的属性和方法。

var xiaoming = {
    name: '小明'
};

xiaoming.__proto__ = Student;

最后一行代码把xiaoming的原型指向了对象Student,看上去xiaoming仿佛继承Student对象。

xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running ...

如果现在定义一个新的对象Bird,然后让xiaoming的原型指向Bird

var Bird = {
    fly: function () {
        console.log (this.name + 'is flying ...');   
    }
};

xiaoming.__proto__ = Bird;

这时xiaoming已经无法run()了,他已经变成了一只鸟:

xiaoming.fly(); // 小明 is flying ...

注意上面直接修改obj.__proto__的做法在开发时不可取,而且低版本的IE不支持这种写法。现在我们理解了面向对象编程的原理,接下来介绍推荐的面向对象编程方法。

面向对象编程

原型链

// 原型对象:
var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有。

var new_student = Object.create(Student);
new_student; //{}
new_student.height; // 1.6
new_student.name; // 'robot'

可以看出,尽管新的对象没有自己的属性,但实际上具有了Student对象的所有属性。基于此我们可以猜想,在获取对象属性时,先在对象内部查找,然后顺着原型依次向上查找。实际就是这样,而且如果访问到最顶层的Object.prototype对象并且还是找不到这个属性,就会返回undefined

这里引入原型链的概念。正如上面所说,JavaScript的每一个对象,其__proto__属性仍是一个对象,因此可以形成一条原型链。以内置的Array对象为例,我们可以用[]创建一个Array对象。

比如,

var arr = [1, 2, 3];

其原型链是:

arr ----> Array.prototype ----> Object.prototype ----> null

Array.prototype定义了indexOf()shift()等方法,因此我们可以在所有的Array对象上直接调用这些方法。

再举一个例子,我们可以用function关键字创建函数。

function foo () {
    return 0;
}

函数也是一个对象,它的原型链是:

foo ----> Function.prototype ----> Object.prototype ----> null

由于Function.prototype定义了apply()等方法,因此所有函数都可以调用apply()方法。

很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。

当然,我们可以把Object.create这个方法包装成一个创建新对象的函数。

function createStudent(name) {
    // 基于Student原型创建一个新对象:
    var s = Object.create(Student);
    // 初始化新对象:
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true

构造函数

除了直接用{...}创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。用法是定义一个构造函数,比如:

function Student (name) {
    this.name = name;
    this.hello = function () {
        alert ('Hello, ' + this.name + '!');
    }
}

这个函数虽然看上去和普通函数一样,但只要用关键字new调用这个函数,它就默认是构造函数,且在函数结束后一定返回this,无论在函数中是否写有return返回语句。

调用的写法如下:

var xiaoming = new Student ('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!

值得注意的是,这个函数如果不写new并调用,则成为了一个返回undefined的普通函数。

这样新建的xiaoming的原型链是:

xiaoming ----> Student.prototype ----> Object.prototype ----> null

此外,用new Student()类似语句创建的对象还从原型上获得了一个constructor属性,它指向Student本身。这段话用代码表示如下。

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true

用图片表示如下,其中红色代表原型链:

对象共享方法

现在这么写,有一个问题:

xiaoming = new Student ('小明');
xiaohong = new Student ('小红');

xiaoming.hello === xiaohong.hello; // false

两个对象的方法不相等,显然浪费了内存空间,因为对于函数而言,我们只需要保存一份就可以了。存在两份的原因是每次调用new Student(),都会在构造时执行var hello一句。对这个问题,一个可行的优化是把hello的定义放在xiaomingxiaohong公共的原型上,而不是构造函数里,这样在调用hello时,就会通过原型链查找到hello。具体的代码如下:

function Student (name) {
    this.name = name;
}

Student.prototype.hello = function () {
   alert ('Hello, ' + this.name + '!');
};

优化的原理通过上面关于原型链的内容很容易理解,即用new Student()语法创建的对象,其原型都指向Student.prototype

当然了,即使有了方便的构造函数工具,我们仍然建议将new的过程封装在函数里完成。原因有二,一是不需要new来调用,避免了漏写new的可能;二是参数更灵活,参数可以不用完整地传递。

原型继承

原文:原型继承 - 廖雪峰的官方网站

function inherits( Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

关于这个函数的内容,请在原文中寻找解释。

这里需要增加的解释是为什么不直接用

PrimaryStudent.prototype = Student.prototype

可以从图中看出,prototype实际指向一个对象,如果用了上面的语句,那么PrimaryStudent就会和Student共享同一个原型对象,这样绑定到PrimaryStudent.prototype上的属性(特别是对象共享的方法)也会被绑定到Student.prototype上,因为它们实际是同一个对象,这样就不符合子类和父类之间的关系。因此可以看出,我们创建的new F()对象,就是为了独立地绑定PrimaryStudent对象共享的方法。

class关键字 [ES6]

在上面的章节中我们看到了JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。

新的关键字class正是为了简化类的定义而引入。对于下面这个用构造函数实现的Student

function Student (name) {
   this.name = name;
}

Student.prototype.hello = function () {
    alert ('Hello, ' + this.name + '!');
};

如果用新的关键字class来实现,可以这样写:

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

显然用class的代码简介明了,既包含了构造函数constructor的定义,也包含了原先定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了代码分散可能造成的理解障碍。

最后,这样定义的Student也是用new关键字调用,得到的对象与之前得到的对象用发法相同。

var xiaoming = new Student ('小明');

class继承 [ES6]

不需要考虑桥接的原型对象,直接用extends关键字完成继承。

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

几个要点:

  • class关键字引导
  • extends关键字引出父类
  • constructor定义开头用super()调用父类的构造方法

由于现在很多浏览器还不支持ES6的所有新特性,特别是class,在这里介绍一个小工具,用于将下一代的JavaScript代码转换为同义的较低版本代码:Babel - The compiler for next generation JavaScript。注:这个工具为原文推荐,而本文写作的时间比参考的文章晚3年,现在的主流数浏览器已经支持了ES6标准。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容