美文网首页
JavaScript | 面向对象编程

JavaScript | 面向对象编程

作者: shawn233 | 来源:发表于2018-09-08 22:59 被阅读0次

    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标准。

    相关文章

      网友评论

          本文标题:JavaScript | 面向对象编程

          本文链接:https://www.haomeiwen.com/subject/qyjlgftx.html