天天看点

JavaScript基础系列之四 面向对象编程

JavaScript基础系列之四 面向对象编程

面向对象编程

JavaScript的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢?

​Number​

​、​

​Array​

​、​

​string​

​以及基本的​

​{...}​

​定义的对象,还无法发挥出面向对象编程的威力。

JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:

​Student​

  1. 类来表示学生,类本身是一种类型,

​Student​

  1. 表示学生类型,但不表示任何具体的某个学生;

​Student​

  1. 类可以创建出

​xiaoming​

​xiaohong​

​xiaojun​

  1. 等多个实例,每个实例表示一个具体的学生,他们全都属于

​Student​

  1. 类型。

所以,类和实例是大多数面向对象编程语言的基本概念。

不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。

​xiaoming​

​这个具体的学生时,我们并没有一个​

​Student​

​类型可用。那怎么办?恰好有这么一个现成的对象:

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

​robot​

​对象有名字,有身高,还会跑,有点像小明,干脆就根据它来“创建”小明得了!​

​Student​

​,然后创建出​

​xiaoming​

​:

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

var xiaoming = {
    name: '小明'      

​xiaoming​

​的原型指向了对象​

​Student​

​,看上去​

​xiaoming​

​仿佛是从​

​Student​

​继承下来的:

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

​xiaoming​

​有自己的​

​name​

​属性,但并没有定义​

​run()​

​方法。不过,由于小明是从​

​Student​

​继承而来,只要​

​Student​

​有​

​run()​

​方法,​

​xiaoming​

​也可以调用:

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。

​xiaoming​

​的原型指向其他对象:

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

xiaoming.__proto__ = Bird;      

​xiaoming​

​已经无法​

​run()​

​了,他已经变成了一只鸟:

xiaoming.fly(); //      

​xiaoming​

​从​

​Student​

​变成​

​Bird​

​,或者变成任何对象。请注意,上述代码仅用于演示目的。在编写JavaScript代码时,不要直接用​

​obj.__proto__​

​去改变一个对象的原型,并且,低版本的IE也无法使用​

​__proto__​

​。​

​Object.create()​

​方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此,我们可以编写一个函数来创建​

​xiaoming​

​:

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

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还有其他方法来创建对象,我们在后面会一一讲到。

创建对象

JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。

​obj.xxx​

​访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到​

​Object.prototype​

​对象,最后,如果还没有找到,就只能返回​

​undefined​

​。​

​Array​

​对象:

var arr = [1, 2, 3];      

其原型链是:

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

​Array.prototype​

​定义了​

​indexOf()​

​、​

​shift()​

​等方法,因此你可以在所有的​

​Array​

​对象上直接调用这些方法。

当我们创建一个函数时:

function foo()
    return 0;
}      

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

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

​Function.prototype​

​定义了​

​apply()​

​等方法,因此,所有函数都可以调用​

​apply()​

​方法。

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

构造函数

​{ ... }​

​创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。它的用法是,先定义一个构造函数:

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

你会问,咦,这不是一个普通函数吗?

​new​

​来调用这个函数,并返回一个对象:

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

注意,如果不写​

​new​

​,这就是一个普通函数,它返回​

​undefined​

​。但是,如果写了​

​new​

​,它就变成了一个构造函数,它绑定的​

​this​

​指向新创建的对象,并默认返回​

​this​

​,也就是说,不需要在最后写​

​return this;​

​。​

​xiaoming​

​的原型链是:

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

​xiaoming​

​的原型指向函数​

​Student​

​的原型。如果你又创建了​

​xiaohong​

​、​

​xiaojun​

​,那么这些对象的原型与​

​xiaoming​

​是一样的:

xiaoming ↘
xiaohong -→ 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      

看晕了吧?用一张图来表示这些乱七八糟的关系就是:

​Student.prototype​

​指向的对象就是​

​xiaoming​

​、​

​xiaohong​

​的原型对象,这个原型对象自己还有个属性​

​constructor​

​,指向​

​Student​

​函数本身。​

​Student​

​恰好有个属性​

​prototype​

​指向​

​xiaoming​

​、​

​xiaohong​

​的原型对象,但是​

​xiaoming​

​、​

​xiaohong​

​这些对象可没有​

​prototype​

​这个属性,不过可以用​

​__proto__​

​这个非标准用法来查看。​

​xiaoming​

​、​

​xiaohong​

​这些对象“继承”自​

​Student​

​。

不过还有一个小问题,注意观察:

xiaoming.name; // '小明'
xiaohong.name; // '小红'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false      

​xiaoming​

​和​

​xiaohong​

​各自的​

​name​

​不同,这是对的,否则我们无法区分谁是谁了。​

​xiaoming​

​和​

​xiaohong​

​各自的​

​hello​

​是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的!​

​new Student()​

​创建了很多对象,这些对象的​

​hello​

​函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存。​

​hello​

​函数,根据对象的属性查找原则,我们只要把​

​hello​

​函数移动到​

​xiaoming​

​、​

​xiaohong​

​这些对象共同的原型上就可以了,也就是​

​Student.prototype​

​:

修改代码如下:

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

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

​new​

​创建基于原型的JavaScript的对象就是这么简单!

忘记写new怎么办

​new​

​怎么办?​

​this.name = name​

​将报错,因为​

​this​

​绑定为​

​undefined​

​,在非strict模式下,​

​this.name = name​

​不报错,因为​

​this​

​绑定为​

​window​

​,于是无意间创建了全局变量​

​name​

​,并且返回​

​undefined​

​,这个结果更糟糕。​

​new​

​。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,这样,一些语法检查工具如​​​jslint​​​将可以帮你检测到漏写的​

​new​

​。​

​createStudent()​

​函数,在内部封装所有的​

​new​

​操作。一个常用的编程模式像这样:

function Student(props)
    this.name = props.name || '匿名'; // 默认值为'匿名'
    this.grade = props.grade || 1; // 默认值为1
}

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

function createStudent(props)
    return new      

​createStudent()​

​函数有几个巨大的优点:一是不需要​

​new​

​来调用,二是参数非常灵活,可以不传,也可以这么传:

var xiaoming = createStudent({
    name: '小明'
});

xiaoming.grade; // 1      

​JSON​

​拿到了一个对象,就可以直接创建出​

​xiaoming​

​。

原型继承

在传统的基于Class的语言如Java、C++中,继承的本质是扩展一个已有的Class,并生成新的Subclass。

由于这类语言严格区分类和实例,继承实际上是类型的扩展。但是,JavaScript由于采用原型继承,我们无法直接扩展一个Class,因为根本不存在Class这种类型。

​Student​

​构造函数:

function Student(props)
    this.name = props.name || 'Unnamed';
}

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

​Student​

​的原型链:

​Student​

​扩展出​

​PrimaryStudent​

​,可以先定义出​

​PrimaryStudent​

​:

function PrimaryStudent(props)
    // 调用Student构造函数,绑定this变量:
    Student.call(this, props);
    this.grade = props.grade || 1;
}      

​Student​

​构造函数不等于继承了​

​Student​

​,​

​PrimaryStudent​

​创建的对象的原型是:

new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null      

必须想办法把原型链修改为:

new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null      

​PrimaryStudent​

​创建的对象不但能调用​

​PrimaryStudent.prototype​

​定义的方法,也可以调用​

​Student.prototype​

​定义的方法。

如果你想用最简单粗暴的方法这么干:

PrimaryStudent.prototype = Student.prototype;      

​PrimaryStudent​

​和​

​Student​

​共享一个原型对象,那还要定义​

​PrimaryStudent​

​干啥?​

​Student.prototype​

​。为了实现这一点,参考道爷(就是发明JSON的那个道格拉斯)的代码,中间对象可以用一个空函数​

​F​

​来实现:

// PrimaryStudent构造函数:
function PrimaryStudent(props)
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 空函数F:
function F()
}

// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;

// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();

// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function ()
    return this.grade;
};

// 创建xiaoming:
var xiaoming = new PrimaryStudent({
    name: '小明',
    grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2

// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true

// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true      

用一张图来表示新的原型链:

​F​

​仅用于桥接,我们仅创建了一个​

​new F()​

​实例,而且,没有改变原有的​

​Student​

​定义的原型链。​

​inherits()​

​函数封装起来,还可以隐藏​

​F​

​的定义,并简化代码:

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

​inherits()​

​函数可以复用:

function Student(props)
    this.name = props.name || 'Unnamed';
}

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

function PrimaryStudent(props)
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 实现原型继承链:
inherits(PrimaryStudent, Student);

// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function ()
    return this.grade;
};      

小结

JavaScript的原型继承实现方式就是:

​call()​

  1. 调用希望“继承”的构造函数,并绑定

​this​

​F​

  1. 实现原型链继承,最好通过封装的

​inherits​

  1. 函数完成;
  2. 继续在新的构造函数的原型上定义新方法。

class继承

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

有没有更简单的写法?有!

​class​

​从ES6开始正式被引入到JavaScript中。​

​class​

​的目的就是让定义类更简单。​

​Student​

​的方法:

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

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

​class​

​关键字来编写​

​Student​

​,可以这样写:

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

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

​class​

​的定义包含了构造函数​

​constructor​

​和定义在原型对象上的函数​

​hello()​

​(注意没有​

​function​

​关键字),这样就避免了​

​Student.prototype.hello = function () {...}​

​这样分散的代码。​

​Student​

​对象代码和前面章节完全一样:

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

class继承

​class​

​定义对象的另一个巨大的好处是继承更方便了。想一想我们从​

​Student​

​派生一个​

​PrimaryStudent​

​需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过​

​extends​

​来实现:

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

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

继续阅读