JavaScript的面向对象

概述

在典型的OOP语言中(如JAVA),都存在类的概念,类是对象的模版,对象是类的实例。当在ES6之前,JS并没有引入类的概念。
在ES6之前,对象不是基于类创建的,而是用一种成为__构建函数__的特殊函数来定义对象和它们的特征。

创建对象的三种方式

对象字面量(无法复用)

var obj = {};

new Object(无法复用)

var obj = new Object();

自定义构造函数

    function Star(name, age) {
        this.name = name;
        this.age = age;
        this.sing = function () {
            console.log('我会唱歌');
        }
    }
    let ldh = new Star('刘德华', 18);
    ldh.sing();
    let zxy = new Star('张学友', 20);
    zxy.sing();

__构成函数__是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象总一些公共的属性和方法抽取出来,然后封装到这个函数里面。

通过对构造函数执行new命令时会做的四件事情:

  1. 在内存总创建一个新的空对象。
  2. 让this指向这个新对象。
  3. 执行构造函数里的代码,给这个新对象添加属性和方法。
  4. 返回这个对象(只是构造函数里面不需要写return)。

静态成员和实例成员:

  1. 静态成员:在构造函数上添加的成员称为静态成员。只能由构造函数本身来访问。
  function Star(name, age) {
     this.name = name;
     this.age = age;
     this.sing = function () {
         console.log('我会唱歌');
     }
 }
 let ldh = new Star('刘德华', 18);
 // 创建一个变量名为gender的静态成员
 Star.gender = '男';
 console.log(Star.gender); // 男
 console.log(ldh.gender); // undefined
  1. 实例成员:在构造函数内部创建的对象成员称为实例成员,只能通过实例化的对象来访问。

自定义构造函数创建对象的问题

构造函数方法虽然好用,但是存在浪费内存空间的问题。对于通过同一个构造函数创建出的实例对象,其中的复杂数据类型都会再开辟一块内存空间来存放,而不会复用。通过使用构造函数原型prototype即可解决以上问题。

构造函数原型prototype

构造函数通过原型分配的函数是对所有对象__共享的__(内存复用)。
JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象总的所有的属性和方法,都会被构造函数说拥有。
我们可以把哪些不变的方法,直接定义在prototype对象上,这样所有的实例对象就可以共享这些方法了。

  function Star(name, age) {
        this.name = name;
        this.age = age;
        // this.sing = function () {
        //     console.log('我会唱歌');
        // }
    }
    // prototype是函数才会有的属性
    // 向Star构造函数的原型对象中添加方法
    Star.prototype.sing = function(){
        console.log('我会唱歌');
    }
    let ldh = new Star('刘德华', 18);
    ldh.sing();
    let zxy = new Star('张学友', 20);
    zxy.sing();
    console.log(ldh.sing === zxy.sing) // true

对象原型 __proto__

对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__ 原型的存在。

   function Star(name, age) {
        this.name = name;
        this.age = age;
        this.sing = function () {
            console.log('wowowo我会唱歌');
        }
    }
    // 向Star构造函数的原型对象中添加方法
    Star.prototype.sing = function(){
        console.log('我会唱歌');
    }
    let ldh = new Star('刘德华', 18);
    ldh.sing(); // wowowo我会唱
    console.log(ldh.__proto__ === Star.prototype); // true

实例对象方法的查找规则:首先会先看当前实例对象的内部是否存在xx方法,如果有就去执行这个对象上的xx方法。如果对象上没有定义这个方法,因为实例对象上有__proto__的存在,就去构造函数的prototype对象中查找xx方法。

  • __proto__对象原型和原型对象prototype是等价的。
  • __proto__对象原型的意义就在于为对象的查找机制提供了一个方向,或者说一条路线。但是它是一个非标准的属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype。

contructor 构造函数

对象原型( __proto__)和构造函数(prototype)原型对象里面都有一个consgructor的属性,constructor我们称之为构造函数,因为它指回构造函数本身。
constructor主要用于记录该对象应用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
__(很多情况下,我们需要手动利用constructor)__这个属性指回原来的构造函数。

    function Star(name, age) {
        this.name = name;
        this.age = age;
    }
    Star.prototype = {
        construct: Star,
        sing: function () {
            console.log('我会唱歌儿');
        },
        movie: function () {
            console.log('我会拍电影');
        }
    }
    let ldh = new Star('刘德华', 18);
    let zxy = new Star('张学友', 20);
    console.log(Star.prototype)
    ldh.sing();
    zxy.movie();

当一个函数被创建时,它的prototype属性也被创建,且该原型对象的construct属性指向该函数,当使用对象字面量形式改写原型对象Star.prototype时,则该constructor指向的是Object,为了避免这一点,需要手动改写原型对象。即通过手动设置construct的属性。

构造函数、实例、原型对象(prototype)三者之间的关系

原型链


JavaScript的成员查找机制:

  1. 当访问一个对象的属性或方法时,首先查找这个__对象自身__中属否存在该属性。
  2. 如果没有就查找它的原型(也就是实例对象__proto__指向的__prototype原型对象__)。
  3. 如果依旧没有找到就继续去向上查找原型对象的原型(Object的原型对象)。
  4. 以此类推一直找到Object为止(null)。

原型对象中this指向

    function Star(name, age) {
        // 在构造函数中的this指向的是对象实例
        this.name = name;
        this.age = age;
    }
    let that;
    // Star构造函数的原型对象
    Star.prototype.sing = function () {
        that = this;
        //this指向最终的调用对象
        console.log('我会唱歌');
    }
    var ldh = new Star('刘德华', 18);
    console.log(ldh.sing());
    console.log(that);
  1. 在构造函数中,里面this指向的是实例对象。
  2. 原型对象函数里的this,指向的是最终调用该函数的实例对象。

扩展内置对象的方法

    // 通过构造函数的原型对象扩展原有的功能
    Array.prototype.sum = function () {
        let sum = 0;
        for (let i = 0; i < this.length; i++) {
            sum += this[i];
        }
        return sum;
    }
    // 不允许采用这种方式书写
    // Array.prototype = {
    //     constructor: Array,
    //     sum: function () {
    //         let sum = 0;
    //         for (let i = 0; i < this.length; i++) {
    //             sum += this[i];
    //         }
    //         return sum;
    //     }
    // }
    let nums = [1, 2, 3, 4];
    console.log(nums.sum());

继承

ES6之前没有提供extends继承。我们可以通过构造函数+原型对象模拟实现继承,这种方式被称为组合继承。

call

调用这个函数,并自定义函数运行时的this指向

func.call(thisArg, arg1, arg2, ...)
  • thsiArg: 当前调用函数的this所要指向的对象。
  • arg1,arg2: 向函数中传递的参数。

借用父构造函数继承属性

   // 1. 父构造函数
    function Father(uname, age){
        this.uname = uname;
        this.age = age;
    }

    // 2. 子构造函数
    function Son(uname, age, school) {
        this.school = school;
        // 当通过call方法调用Father构造函数的时候,运行时,Father中的this指向的是son,那么对uname和age的赋值就是作用在son上的。
        Father.call(this, uname, age);
    }

    let son = new Son('刘德华', 18, "不知道哦");
    console.log(son.uname);
    console.log(son.age);
    console.log(son.school);

真的是继承么?

继承意味着复制,然而JavaScript默认不会复制对象的属性,相反,JavaScript只是在两个对象之间创建了一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承不如说是委托准确些。