JavaScript学习总结(一)——类和继承

  • Post author:
  • Post category:java




一、继承



1、概念
  • f.prototype原型:函数的原型,当用函数f作为构造函数创造实例时,实例instance会继承f.prototype;
  • obj.__ proto__原型对象:一种浏览器支持的非标准方式;obj可以为任何对象,obj2.__ proto__=obj;就直接表示obj2继承自obj。

    ####2、继承的语法
  1. ES5定义了obj2 = Object.create(obj)方法来实现继承关系;调用此函数后obj2继承自obj;
  2. 针对没有Object.create()方法的ES3,我们定义了下面inherit函数;通过这个函数的实现,可以看出继承的关键:

    1. 首先定义一个构造函数f;
    2. f.prototype = p;f的原型为p
    3. 构造f的实例new f();
  3. obj2.__ proto=obj;可以直接实现obj2继承自obj的语法

    A继承了B,则:
  • 如果在A中查询属性x,如果A中不存在,则在A的原型对象B中查找;如果B中也没有,则在B的原型对象中查找,以此类推,直到原型对象链结束。
  • 如果给A中属性x赋值:

    1. 如A中有已有x(不是继承来的),那么就直接改变这个x的值(受writable特性的限制);
    2. 如A中没有x,而其原型对象链中有(且可写),那么A中添加x,并给它赋值;而它会覆盖原型对象链中的x(以后通过A访问x都是这个x);
    3. 如A中没有x,而其原型对象链中有(但不可写),赋值操作不造成任何影响;
    4. 对getter和setter进行操作时,writable特性,是依据getter和setter内部修改的实际属性。
  • 查询操作会受继承的影响,赋值操作只是对对象本身的修改(当原型对象链中有不可修改的属性时,对象中不能添加此属性);
/***********************************************
 * inherit()返回一个继承自原型对象p的属性的新对象
 * 这里使用ECMAScript 5中的Object.create()函数(如果存在的话)
 * 如果不存在Object.create()方法,则退化使用其他方法
 ***********************************************/
function inherit(p) {

    if (Object.create) return Object.create(p); //使用Object.create方法
    /* 检测p的类型是否符合要求 */
    var t = typeof p;
    if (t !== 'object' && t !== 'function') throw TypeError();
    /* 下面的原型创建方法,类似于Object.create(p);  */
    function f() {};
    f.prototype = p;
    return new f();
}
/***
 * 此例中定义了对象a,具有可修改的属性x和存取器y
 * 此例中定义了对象b,具有不可修改的属性x和存取器y
 * c继承自a,d继承自b;调用c和d的setter后,观察对象getter返回值的变化
 * */
//a有属性x(可读写),存取器属性y(可读写)
var a = { x: 1 };
Object.defineProperty(a, 'y', {
    get: function() {
        return this.x + 1;
    },
    set: function(x) {
        this.x = x - 1;
    },
    configurable: true,
});
//b有属性x(不可写),存取器属性y(可读写)
var b = {};
Object.defineProperty(b, 'x', { value: 1, configurable: true });
Object.defineProperty(b, 'y', {
    get: function() {
        return this.x + 1;
    },
    set: function(x) {
        this.x = x - 1;
    },
    configurable: true,
})

var c = Object.create(a);
c.y = 3;
var d = Object.create(b);
d.y = 3;
//由于a中x可读写,所以给c添加一个熟悉x;而b中x不可写,调用setter没有任何效果
console.log(a.y); //2
console.log(c.y); //3
console.log(b.y); //2
console.log(d.y); //2




二、类



1、JavaScript中的类型

JavaScript中只有两大类型:原始值和对象;类型通过typeof运算符来得到



1.1、原始值

原始值的特点是不可修改(在那个特定的内存区域保存的就是这个原始值的值);我们可以让变量指向另一个原始值,但无法改变原始值内部的内容。

  1. undefined
  2. null
  3. Number
  4. Boolean
  5. String
  6. Symbol: 一种特殊的string序列(独一无二)

    #####1.2、对象
  7. 对象本身就是一个引用,它指向一个集合的地址,集合内是对象的属性;我们修改集合内的对象属性,不改变对象的值,只有当我们将对象指向另一个集合时,才会改变对象的值。
  8. JavaScript作为一个弱类型的语言,它的对象类型非常灵活。面对如此庞杂的对象,我们要识别它,就引入了类的概念。

    ####2、类的概念

    #####2.1、类的基本组成

    类,顾名思义,就是对对象进行分类和抽象,它主要有下面几部分的内容:
  9. 实例字段:它们是基于实例的属性或变量,用以保存对立对象的状态;在js中,就是构造函数中this的属性。
  10. 实例方法:他们是类的所有实例所共享的方法,由每个独立的实例调用;在js中,通过原型来实现,就是constructor.prototype的方法;
  11. 类字段: 这些属性或变量属于类的,而不是属于类的某个实例的;在js中,就是constructor.properties;
  12. 类方法: 这些方法属于类的,而不是属于类的某个实例的;在js中,就是constructor.methods;

上面的2、3、4项,都是为了实现某种类型的共享。区别主要有:实例方法,可以在函数内部直接使用this来访问实例的属性和方法;而类的方法和字段,类似于全局函数和变量,作为构造函数的属性,只是在标识符上用类名做了一个区分。



2.2、类的各部分关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M1IQ1eJb-1593672875475)(./asset/proto.jpg)]

以Persion类为例,令 proto = Persion.prototype;类需要具备以下条件:

  1. proto.constructor = Persion;
  2. Persion.prototype = proto;
  3. instance = Object.create(proto);在构造函数里,我们通过new来实现这个关系,在工厂函数中,我们可以通过手动实现。

上面的第三条是类的根本,要检验 instance instanceof Persion 实际上就是 instance的继承链上是否有Persion.prototype,无论是不是直接继承。

/**factory method
 * 一个简单的范围类
 */
function range(from, to) {
    var r = Object.create(range.prototype);//上面的条件3
    r.from = from;
    r.to = to;
    return r;
}
range.prototype = {//上面的条件2
    constructor: range, //上面的条件1
    includes: function(x) {
        return this.from <= x && x <= this.to;
    },
    foreach: function(f) {
        for (var x = Math.ceil(this.from); x <= this.to; ++x) f(x);
    },
    toString: function() {
        return '(' + this.from + '...' + this.to + ')';
    }
};
var r = range(1, 3);
console.log(r instanceof range);//true;满足了上面三个条件,所以r是range的实例



2.3、类的各部分关系的再深入

javaScript中,类主要由constructor、constructor.prototype两部分组成。constructor有两部分:构造函数本身,创建instance数据;constructor的属性是类的静态方法和数据

/**一个定义简单类的函数 */
/**extend 
 * 拓展对象o中的属性(只处理可枚举属性,包括继承来的属性)
 * 如果o与p中有同名属性,则覆盖o中属性
 * @param {Object} o 
 * @param {Object} p
 * @return {Object} 
 */
function extend(o, p) {
    for (var prop in p) {
        o[prop] = p[prop]; //浅复制
    }
    return o;
}

/**defineClass
 * 通过构造函数constructor、实例方法method、类数据和类方法static来构造类
 * @param {Function} constructor 
 * @param {Object} method 
 * @param {Object} static 
 * @return {Function}
 */
function defineClass(constructor, method, static) {
    if (method) extend(constructor.prototype, method);
    if (static) extend(constructor, static);
    return constructor;
}

//constructor
function Complex(real, imaginary) {
    if (isNaN(real) || isNaN(imaginary)) throw TypeError("real and imaginary must be number!");
    this.r = real;
    this.i = imaginary;
}
//methods
var methodForComplex = {
    add: function(that) {
        return new Complex(this.r + that.r, this.i + that.i);
    },
    toString: function() {
        return '{' + this.r + ',' + this.i + '}';
    },
    equal: function() {
        return that != null && that.constructor === Complex &&
            this.r === that.r && this.i === that.i;
    }
};
//static
var staticForComplex = {
    ZERO: new Complex(0, 0),
    ONE: new Complex(1, 0),
    I: new Complex(0, 1),
    //static method
    polar: function(r, angle) {
        return new Complex(r * Math.cos(angle), r * Math.sin(angle));
    }
};
Complex = defineClass(Complex, methodForComplex, staticForComplex);
var compA = new Complex(2, 3);
var compB = Complex.polar(2, Math.PI / 4);
console.log(compA, compB, compA.add(Complex.I));

/*******************************************************************************
 * ES6类的新语法
 *****************************************************************************/
class Complex_ES6 {
    constructor(real, imaginary) {
        if (isNaN(real) || isNaN(imaginary)) throw TypeError("real and imaginary must be number!");
        this.r = real;
        this.i = imaginary;
    };
    //methods
    add(that) {
        return new Complex_ES6(this.r + that.r, this.i + that.i);
    }
    toString() {
        return '{' + this.r + ',' + this.i + '}';
    }
    equal() {
        return that != null && that.constructor === Complex_ES6 &&
            this.r === that.r && this.i === that.i;
    }

    //static methods
    static polar(r, angle) {
        return new Complex_ES6(r * Math.cos(angle), r * Math.sin(angle));
    }
}
Complex_ES6.ZERO = new Complex(0, 0);
Complex_ES6.ONE = new Complex(1, 0);
Complex_ES6.I = new Complex(0, 1);
var compA = new Complex_ES6(2, 3);
var compB = Complex_ES6.polar(2, Math.PI / 4);
console.log(compA, compB, compA.add(Complex_ES6.I));



三、类的继承



1、子类和继承

继承是为了重利用之前的定义的类及其方法。

  • JavaSript类有下面三方面的内容:
  1. 实例数据:构造函数内定义的数据,每个实例都有独立的一份;
  2. 实例共享方法:constructor.prototype的属性,所有实例共享的方法,在函数内部可以通过this访问实例中的数据
  3. 类数据和方法:所有实例共享的数据和方法,类方法调用方式跟普通函数一样;类数据的调用方法与普通变量一样。只是加了一个类的前缀,方便记忆和区分。

    下面定义一个父类,方便后面继承的使用:
/**MySet类 */
function MySet() {
    this.values = {};
    this.n = 0;
    this.add.apply(this, arguments);
}
MySet._v2s = function(val) {
    switch (val) {
        case undefined:
            return 'u';
        case null:
            return 'n';
        case true:
            return 't';
        case false:
            return 'f';
        default:
            switch (typeof val) {
                case 'number':
                    return '#' + val;
                case 'string':
                    return '"' + val;
                default:
                    return '@' + val.objectId;
            }
    }
};

/**定义不可枚举的属性 */
(function() {
    /**定义一个不可枚举的属性objectId它被所有对象继承
     * 没有定义setter,所以它不可写;同时不可配置,不可删除
     */
    Object.defineProperty(Object.prototype, 'objectId', {
        get: idGetter,
        enumerable: false,
        constructor: false
    });
    //读取objectId时,直接调用这个函数;它返回idprop属性,
    //如果没有此属性,分配给它一个独一无二的值。
    function idGetter() {
        if (!(idprop in this)) {
            Object.defineProperty(this, idprop, {
                value: nextid++,
                writable: false,
                enumerable: false,
                configurable: false
            });
        }
        return this[idprop];
    }
    var idprop = "|**objectId**|";
    var nextid = 1;
}());
MySet.prototype = {
    constructor: MySet,
    add: function() {
        for (var i = 0; i < arguments.length; ++i) {
            var val = arguments[i];
            console.log("MySet._v2s", MySet._v2s);
            var str = MySet._v2s(val);
            if (!this.values.hasOwnProperty(str)) {
                this.values[str] = val;
                ++this.n;
            }
        }
        return this;
    },
    remove: function() {
        for (var i = 0; i < arguments.length; ++i) {
            var val = arguments[i];
            var str = MySet._v2s(val);
            if (this.values.hasOwnProterty(str)) {
                delete this.values[str];
                this.n--;
            }
        }
        return this;
    },
    contains: function(value) {
        var str = MySet._v2s(value);
        return this.values.hasOwnProterty(str);
    },
    size: function() {
        return this.size;
    },
    foreach: function(f, context) {
        for (var s in this.values) {
            if (this.values.hasOwnProterty(s)) {
                f.call(context, this.values[s]);
            }
        }
    }
}

//添加新的方法到MySet类的原型对象中
function extend(o, p) {
    for (var prop in p) {
        o[prop] = p[prop]; //浅复制
    }
    return o;
}
extend(MySet.prototype, {
    toString: function() {
        var s = '{',
            i = 0;
        this.foreach(function(v) {
            s += ((i++ > 0) ? ',' : '') + v;
        });
        return s + '}';
    },
    //类似与toString,但对于所有的值,都调用toLocalString
    toLocalString: function() {
        var s = '{',
            i = 0;
        this.foreach(function(v) {
            if (i++ > 0) s += ',';
            if (v == null) s += v;
            else s += v.toLocalString();
        });
        return s + '}';
    },
    toArray: function() {
        var a = [];
        this.foreach(function(v) {
            a.push(v);
        });
        return a;
    }
});
MySet.prototype.toJSON = MySet.prototype.toArray;

var a = { x: 1 };
var MySet = new MySet(2, 3, '133', 2, a);
console.log(MySet);


2、继承的实现

类继承中对上面三个方面的处理:

  1. 实例数据(构造函数链):在子类构造函数内部调用父类构造函数(在子类构造函数中添加语句Superclass.apply(this,arguments));
  2. 实例共享方法(方法链):子类构造函数的原型继承父类构造函数的原型;constructor.prototype=Object.create(Superclass.prototype);继承后,我们可以通过重载父类中的方法。
  3. 类数据和方法:既然跟普通函数、变量的使用方法一样。那么就可以不用集成,直接通过变量名来使用。

下面的例子定义了一个defineSubclass函数,用来定义子类,它实现了方法链的继承。

/**
 * 一个创建简单子类的函数
 * @param {Function} superclass 
 * @param {Function} constructor 
 * @param {Object} methods 
 * @param {Object} statics 
 * @return {Function}
 */
function defineSubclass(superclass, constructor, methods, statics) {
    //方法链的继承
    constructor.prototype = Object.create(superclass.prototype);
    constructor.prototype.constructor = constructor;
    //重载或添加新的实例方法
    if (methods) extend(constructor.prototype, methods);
    //添加类静态成员
    if (statics) extend(constructor, statics);

    return constructor;
}

Function.prototype.extend = function(constructor, methods, statics) {
    return defineClass.call(this, constructor, methods, statics);
}

下面的例子,使用上面定义的方法,定义了一个上节中Set类的子类NonNullSet

var NonNullSet = (function() {
    var superclass = MySet;//定义父类
    return superclass.extend(
        //constructor(内部有构造函数链的继承)
        function() { superclass.apply(this, arguments); },
        //重构的方法
        {
            add: function() {
                Array.prototype.forEach.call(arguments, function(val) {
                    if (val == null) throw new Error("Can't add null or undefined to a NonNullSet!");
                });
                return superclass.prototype.add.apply(this, arguments);
            }
        }
    );
}());


3、广义继承关系的其他实现方法


3.1、类工厂模式

工厂模式在javacript中应用广泛,是一个深刻的概念。我们这里只介绍它的一种应用情形:创建多种不同的类,而这些类共享大量相同的属性,我们通过此函数,可以创建功能相似的不同类。

/**类工厂
 * 该函数返回一个具体MySet类的子类
 * 重写add()方法,对添加的元素做特殊的过滤
 * @param {Function} superclass 
 * @param {Function} filter 
 * @return {Function}
 */
function filteredSetSubclass(superclass, filter) {
    var constructor = function() {
        superclass.apply(this, arguments);
    };
    var proto = constructor.prototype = Object.create(superclass);
    proto.constructor = constructor;
    proto.add = function() {
        Array.prototype.forEach.call(arguments, function(val) {
            if (!filter(val)) throw ("value " + val + " rejected by filter");
        });
        return superclass.prototype.add.apply(this, arguments);
    }
    return constructor;
}
//使用工厂方法定义子类
var NonNullSet_Factory = filteredSetSubclass(MySet, function(val) {
    return val != null;
});


3.2、组合方法

跟子类相比,组合一种完全不同的继承方式:

  1. 它的构造函数的参数跟父类不同,没有继承关系
  2. 它把父类的实例作为自己的一个属性。而这个实例就连接了它跟父类之间的关系
  3. 它定义与父类相似的方法,在方法内部调用父类的方法,有必要的话,进行适当的改造

下面的函数实现了得到的FilteredSet与上面工厂方法的filteredSetSubclass相似,不同的是FilteredSet的第一个参数是一个父类的实例,返回的是子类的实例,而filteredSetSubclass的地一个参数是父类构造函数,返回的是一个子类的构造函数。

/**组合方法
 * 实现一个FilterSet,它包装某个指定的“集合”对象,
 * 并对传入add()方法的值引用某种指定的过滤器。
 * 集合类的其他所有核心方法,延续到包装后的实例中。
 * 这种方法通过把类对象作为其属性来实现对类方法数据的继承;
 * 注意:它的构造函数的参数跟其组合的类的结构不同
 */

function FilteredSet(set, filter) {
    this.set = set;
    this.filter = filter;
}

FilteredSet.prototype = {
    constructor: FilteredSet,
    add: function() {
        if (this.filter) {
            Array.prototype.forEach.call(arguments, function(val) {
                if (!this.filter(val)) throw ("value " + val + " rejected by filter");
            });

        }
        this.set.add.apply(this.set, arguments);
        return this;
    },
    remove: function() {
        this.set.remove.apply(this.set, arguments);
        return this;
    },
    contains: function(val) {
        return this.set.contains(val);
    },
    size: function() { return this.set.size(); },
    foreach: function(f, c) { this.set.foreach(f, c); }
};



四、ECMAScript5中特性对类的修饰



1、属性特性的方法支持
  • 属性特性是一个对象包括value,writable,enumerable,configurable四个特性;同时也可用setter和getter来代替value和writable特性。
  • 将methods的enumerable属性的设置为false,使他们在遍历中隐藏,与类实际的意义更符合。
  • 通过writable和configurable属性的设置,使部分属性不可修改,从而增加类的安全性。

下面的例子中,首先定义了两个工具函数来修改属性的特性;然后通过这两个函数实现方法在遍历中的隐藏,通过setter和getter方法实现数据成员的封装。

/**EMACScript5中的类
 * 属性特性和对象特性对类的修饰
 */
//属性描述工具函数
//将o指定名字的属性(或所有属性)设置为不可写的和不可配置的
function freezeProps(o) {
    var props = (arguments.length === 1) ?
        Object.getOwnPropertyNames(o) : Array.splice.call(arguments, 1);
    props.forEach(function(prop) {
        if (!Object.getOwnPropertyDescriptor(o, prop).configurable) return;
        Object.defineProperty(o, prop, {
            writable: false,
            configurable: false
        });
    });
    return o;
}
//将o的指定名字的属性(或所有属性)设置为不可枚举的
function hideProps(o) {
    var props = (arguments.length === 1) ?
        Object.getOwnPropertyNames(o) : Array.splice.call(arguments, 1);
    props.forEach(function(prop) {
        if (!Object.getOwnPropertyDescriptor(o, prop).configurable) return;
        Object.defineProperty(o, prop, { enumerable: false });
    });
    return o;
}
/**
 * 将Range类的数据严格封装起来
 * @param {Number} from 
 * @param {Number} to 
 */
function Range(from, to) {
    if (from > to) throw new Error("Range: from must be <= to");

    function getFrom() { return from; }

    function getTo() { return to; }

    function setFrom(f) {
        if (f <= to) from = f;
        else throw new Error("Range: from must be <= to");
    }

    function setTo(t) {
        if (f >= from) to = t;
        else throw new Error("Range: from must be <= to");
    }
    Object.defineProperties(this, {
        from: { getFrom, setFrom, enumerable: true, configurable: false },
        to: { getTo, setTo, enumerable: true, configurable: false }
    });
}

Range.prototype = hideProps({
    constructor: Range,
    includes: function(x) { return this.from <= x && x <= to; },
    foreach: function(f) {
        for (var i = Math.ceil(this.from); i <= this.to; i++) f(i);
    },
    toString: function() { return '(' + this.from + '...' + this.to + ')'; }
});


2、类的拓展性的设置
  • Object.preventExtensions():使对象不可拓展,通过对类原型,构造函数的使用这种方法,可以阻塞使用者给类添加新的方法或者静态方法和数据;
  • Object.seal():除了使对象不可拓展,还使对象的属性都变成不可配置;
  • Object.freeze():除了实现seal的功能外,还使对象不可写;
  • Object.create(superclass,methods):这个函数可以拥有类原型的继承,我们的例子中也有使用过。



五、ECMAScript6中的类语法



1.类的新语法

ES6的类语法实际上是ES5的语法糖

  • 增加class关键词,用于定义类,跟C++/java的语法更相似,class定义的实际上还是一个函数;
  • 在类的内部有:

    1. constructor用来定义构造函数;未定义情况下,系统帮忙定义一个空的构造函数;
    2. 内部定义的函数,作为类原型的方法;
    3. static关键词用来定义静态方法或静态数据(静态数据也可通过className.prop的方式定义);

下面我们定义了一个简单的类

class Person {
    //constructor
    constructor(name, surname, age) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    };
    //Person.prototype.getFullName
    getFullName() {
        return this.name + " " + this.surname;
    };
    //Person.older
    static older(person1, person2) {
        return (person1.age >= person2.age) ? person1 : person2;
    }
}
console.log("typeof Person:", typeof Person);//typeof Person: function


2.类的继承语法

继承有两个关键词extends和super:

  • child extends superclass:这是继承的基本语法
  • super:

    1. 在构造函数内部使用super(args);用来调用父类的构造函数;
    2. super作为superclass.prototype的引用,用了调用父类的方法。

下面是调用对上面类的继承,我们检验了super属性。

class PersonWithMiddlename extends Person {
    constructor(name, middlename, surname, age) {
            //类似于Person.call(this,name,surname,age);但有区别
            super(name, surname, age);
            this.middlename = middlename;
            //证明super是superclass.prototype;但不可直接使用如super===Person.prototype是错的
            console.log(super.getFullName === Person.prototype.getFullName);//
        }
        //重载方法
    getFullName() {
        //调用父类的方法,只是为了演示
        console.log("superclassMethod", super.getFullName());
        return this.name + " " + this.middlename + " " + this.surname;
    }
}
var person = new PersonWithMiddlename('song', 'xiao', 'gao', 48);
console.log("childclassMethod:", person.getFullName());//superclassMethod song gao
//childclassMethod: song xiao gao


3.ES6中类的继承与ES5中类继承的区别
  • ES5中,我们在子类构造函数中调用父类构造函数的方法是:superclass.apply(this,arguments);但对于原生数据类型来说,它有部分属性是[Symbol.unscopables]的属性,这部分属性无法使用with、call、apply来改变其作用域,这样就导致子类无法获得原生构造函数的内部属性。
  • ES6的语法可以解决上面的问题,因为ES6先新建父类的实例对象this,然后再用子类的构造函数修饰this,使的父类的所有行为都可以继承。(这句话,我不太理解)可以看作因为Symbol是ES6引入的,class也是。Symbol引入导致了ES5的问题,ES6解决了。

下面的例子可以看出这种区别

/**
 * 原生构造函数的的this无法绑定,导致拿不到内部属性
 */
function MyArray() {
    Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.constructor = MyArray;
var colors = new MyArray(1);
colors[2] = 'red';
//并未继承到Array的属性
console.log(colors.length, colors[2], colors);//0 red MyArray{'2','red'}
/**
 * ES6允许继承原生构造函数,定义子类
 * 因为ES6先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
 */
class MyArray_ES6 extends Array {
    constructor(...args) {
        super(...args);
    }
}

var colors = new MyArray_ES6(1);
colors[2] = 'red';
//真正对内置类型Array的继承
console.log(colors.length, colors[2], colors);//3 red MyArray_ES6(3) [ <2 empty items>, 'red' ]

声明:本文章实例主要来自《javascript权威指南(第六版)》和《ES6标准入门(第三版)》



版权声明:本文为new_start_2018原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。