zoukankan      html  css  js  c++  java
  • 对象的继承

    原文地址:https://wangdoc.com/javascript/
    JavaScript语言的继承通过class,而是通过原型对象(prototype)实现。

    原型对象概述

    构造函数的缺点

    同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。这个问题的解决方法,就是JavaScript的原型对象。

    prototype属性的作用

    JavaScript继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。JavaScript规定,每个函数都有一个prototype属性,指向一个对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.color = "white";
    
    var cat1 = new Animal("大毛");
    var cat2 = new Animal("二毛");
    
    cat1.color // "white"
    cat2.color // "white"
    
    Animal.prototype.color = "yellow";
    cat1.color // "yellow"
    cat2.color // "yellow"
    
    cat1.color = "black";
    cat1.color // "black"
    cat2.color // "yellow"
    

    原型链

    JavaScript规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所有它也有自己的原型。因此就会形成一个原型链:对象到原型,再到原型的原型...
    如果一层层的上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。那么Object.prototype对象有没有它的原型呢,回答是Object.prototype的原型是nullnull没有属性和方法,也没有自己的原型。因此,原型链的尽头是null
    读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到Object.prototype还是找不到,就会返回undefined。如果对象自身和它的原型都定义了一个属性,那么优先读取对象自身的属性,这叫做覆盖(overriding)。
    注意:一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果某个不存在的属性,将会遍历整个原型链

    MyArray.prototype = new Array();
    MyArray.prototype.constructor = MyArray;
    
    var mine = new MyArray();
    mine.push(1, 2, 3);
    mine.length // 3
    mine instanceof Array // true
    

    constructor属性

    prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

    function P() {}
    var p = new P();
    
    p.constructor === P // true
    p.constructor === P.prototype.constructor // true
    p.hasOwnProperty("constructor") // false
    

    constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

    function F() {}
    var f = new F();
    f.constructor === F // true
    f.constructor === RegExp // false
    

    constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错

    function Person(name) {
        this.name = name;
    }
    
    person.prototype.constructor === Person // true
    
    Person.prototype = {
        method: function () {}
    };
    
    Person.prototype.constructor === Person // false
    Person.prototype.constructor === Object // true
    

    所以,修改原型对象时,一般要同时修改constructor属性的指向。

    // 坏的写法
    C.prototype = {
        method: function () {}
    };
    
    // 好的写法
    C.prototype = {
        constructor: C,
        method: function () {}
    };
    
    // 更好的写法
    C.prototype.method = function () {}
    

    要么将constructor属性重新指向原来的构造函数,要么只在原型对象上添加方法。

    instanceof运算符

    instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。

    var v = new Vehicle();
    v instanceof Vehicle // true
    

    instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象,是否在左边对象的原型链上。

    v instanceof Vehicle
    // 等同于
    Vehicle.prototype.isPrototypeOf(v)
    

    有一种特殊情况,就是在左边对象的原型链上,只有null对象。这时,instanceof判断会失真。

    var obj = Object.create(null);
    typeof obj // "object"
    Object.create(null) instanceof Object // false
    

    上面代码中Object.create(null)返回一个新对象obj,它的原型是null
    instanceof运算符的一个用处,是判断值的类型。

    var x = [1, 2, 3];
    var y = {};
    x instanceof Array // true
    y instanceof Object // true
    

    上面代码中,instanceof运算符判断,变量x是数组,变量y是对象。
    注意,instanceof只能用于对象,不适用原始类型。

    构造函数的继承

    让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。
    function Sub(value) {
    Super.call(this);
    this.prop = value;
    }

    上面代码中,<font color=red>Sub</font>是子类的构造函数,<font color=red>this</font>是子类的实例。在实例上调用父类的构造函数<font color=red>Super</font>,就会让子类实例具有父类实例的属性。
    第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.prototype.method = "..."
    

    上面代码中,Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型super.prototype一起修改掉。
    另一种写法是Sub.prototype等于一个父类实例。

    Sub.prototype = new Super();
    

    上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐这种写法。

    多重继承

    JavaScript不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。

    function M1() {
        this.hello = "hello";
    }
    
    function M2() {
        this.world = "world";
    }
    
    function s() {
        M1.call(this);
        M2.call(this);
    }
    
    // 继承 M1
    S.prototype = Object.create(M1.prototype);
    // 继承链上加入M2
    Object.assign(S.prototype, M2.prototype);
    
    S.prototype.constructor = S;
    
    var s = new S();
    s.hello // "hello"
    s.world // "world"
    

    模块

    JavaScript不是一种模块化编程语言,ES6才开始支持类和模块。下面介绍传统的做法。

    基本的实现方法

    简单的做法是把一个模块写成一个对象,所有的模块成员都放在这个对象里面。

    var module1 = new Object({
        _count: 0,
        m1: function () {},
        m2: function () {}
    });
    

    但是这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

    module1._count = 5;
    

    封装私有变量:构造函数的写法

    我们可以利用构造函数,封装私有变量。

    function StringBuilder() {
        var buffer = [];
    
        this.add = function (str) {
            buffer.push(str);
        };
    
        this.toString = function () {
            return buffer.join("");
        };
    }
    

    上面代码中,buffer是模块的私有变量。一旦生成实例对象,外部是无法直接访问buffer的。但是这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除buffer中已有的值。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上分离的原则(即实例对象的数据,不应该存放在实例对象以外)。

    function StringBuilder() {
        this._buffer = [];
    }
    
    StringBuilder.prototype = {
        constructor: StringBuilder,
        add: function (str) {
            this._buffer.push(str);
        },
        toString: function () {
            return this._buffer.join("");
        }
    };
    这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。
    ### 封装私有变量:立即执行函数的写法
    一种做法是使用立即执行函数,将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。
    

    var module1 = (function () {
    var _count = 0;
    var m1 = function () {
    // ...
    };
    var m2 = function () {};
    return {
    m1: m1,
    m2: m2
    };
    })();
    外部代码无法读取内部的_count变量。

    console.log(module1._count); // undefined
    

    上面的module1就是JavaScript模块的基本写法。

    模块的方法模式

    如果一个模块很大,必须分成几部分,或者一个模块需要继承另一个模块,这时就有必要采用放大模式。

    var module1 = (function (mod) {
        mod.m3 = function () {};
        return mod;
    })(module1);
    

    上面的代码以module1,运行匿名函数。为module1模块添加了新方法m3(),然后返回新的module1模块。
    在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道那个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在的空对象,这时就要采用宽放大模式。

    var module1 = (function (mod) {
        // ...
        return mod;
    })(window.module1 || {});
    

    与放大模式相比,宽放大模式就是立即执行函数的参数可以是空对象。

    输入全局变量

    独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
    为了在模块内部调用全局变量,必须显示地将其他变量输入模块。

    var module1 = (function ($, YAHOO) {
        // ...
    })(jQuery, YAHOO);
    

    上面的module1模块需要使用Jquery库和YUI库,就把这两个库,其实是两个模块,当做参数输入module1。这样做除了保证模块的独立性,还使得模块之间依赖关系变得明显。
    立即执行函数还可以起到命名空间的作用。

    (function ($, window, document) {
        function go(num) {}
    
        function handleEvents() {}
    
        window.finalCarouse1 = {
            init: initialize,
            destroy: dieCarouseDie
        }
    })(jQuery, window, document);
    

    上面代码中,finalCarousel对象输出到全局,对外暴露initdestroy接口,内部方法go...外部无法调用。

  • 相关阅读:
    Sql Server2012连接不上问题
    WinForm的App.config写法
    (原)centos 防火墙开放端口命令
    Linux下rsync的用法
    Centos7下部署minio
    arthas简单使用
    在Docker中运行MinIO单点模式
    中台是什么
    Kubernetes K8S之Ingress详解与示例
    Rancher2.x平台搭建与使用
  • 原文地址:https://www.cnblogs.com/chris-jichen/p/10145030.html
Copyright © 2011-2022 走看看