【读】JavaScript之面向对象

本篇是JavaScript高级程序设计第三版第六章《面向对象的程序设计》阅读记录。若有疑问能够联系我html

理解对象

根据ECMA-262,对象为无序属性的集合,其属性能够包含基本值、对象、或函数。下面是个例子:数组

var person = new Object();
person.name = 'GY';
person.age = 18;
person.sayHi = function() {
    alert('Hi!')
};

// 或者能够这样

var person = {
    name: 'GY',
    age: 18,
    sayHi: function() {
        alert('Hi!');
    }
};
复制代码

这里完美的展示了JavaScript对象无序属性的集合的定义。浏览器

属性类型

ECMAScript中有两种属性:数据属性 和 访问器属性。bash

用来描述属性(property)各类特征的,称之为特性(attribute)。对比属性描述对象。这些特性不能直接访问,经常使用[[name]]来描述,好比[[configurable]]闭包

  • 数据属性app

    能够写入和读取值。该种属性有4个特性:函数

    • [[value]],写入和读取时,都操做的是这里。默认值undefined
    • [[configurable]],表示是否能经过delete从对象中删除该属性、可否修改该属性的特性、可否把属性修改成访问器属性。默认值true
    • [[enumerable]],表示是否能经过for-in遍历该属性。默认值true
    • [[writable]],表示是否能够修改该属性的值。默认值true

    好比上面定义的person,其中的属性值,都存储在其[[value]]特性中。ui

    那么如何操做这些特征值呢?先来看看如何修改,使用Object.defineProperty方法。this

    /// 参数依次为:
    /// 须要操做的对象(这里为person),
    /// 须要操做的属性(这里为name),
    /// 特征值(类型为对象)
    Object.defineProperty(object, 'property', attributes);
    复制代码

    好比对对于上面的person对象,咱们写出以下代码spa

    Object.defineProperty(person, 'name', {
        writable: false,
        value: 'OtherName'
    });
    
    console.log(person.name); // OtherName
    person.name = '任意值';
    console.log(person.name); // OtherName
    复制代码

    这里经过Object.definePropertyname属性的writable特性定义为false,那么name属性将为只读属性。没法再次赋值。对writable特性为false的属性赋值,非严格模式下会忽略,严格模式下会抛出错误Cannot assign to read only property 'name' of object

    相应的,能够经过该方法修改configurableenumerable特性。值得注意的是,对configurable设置为false后,将致使configurableenumerable不能再次更改。

    Object.defineProperty(person, 'name', {
        configurable: false
    });
    
    console.log(person.name); // GY
    // delete person.name; // 严格模式会报错
    
    // 在configurable: false这里将不能再次修改
    // Cannot redefine property: name at Function.defineProperty
    Object.defineProperty(person, 'name', {
        configurable: true,
        enumerable: true,
        
    });
    复制代码
  • 访问器属性

    该类型属性不存储值,没有[[value]]。包含getset函数(这二者都是非必须的)。在读取和写入时将调用对应函数。访问器属性有4个特性:[[configurable]][[enumerable]][[get]][[set]].

    访问器属性不能直接定义,须要使用Object.defineProperty,好比:

    var person = {
        // 下划线一般表示须要经过对象方法访问。规范!
        _age: 0
    };
    
    Object.defineProperty(person, 'age', {
        get: function() {
            return this._age;
        },
        set: function(v) {
            this._age = v >= 0 ? v : 0;
        }
    });
    复制代码

    如只提供get,意味着该属性只读;只提供set,读取会返回undefined

    若你想一次定义多个属性及其特性,可使用Object.defineProperties,向下面这样:

    var person = {
        // 下划线一般表示须要经过对象方法访问。规范!
        _age: 10
    };
    
    Object.defineProperties(person, {
        age: {
            get: function() {
                return this._age;
            },
            set: function(v) {
                this._age = v >= 0 ? v : 0;
            }
        },
        name: {
            value: 'unnamed',
            writable: true,
            enumerable: true
        }
    });
    复制代码
  • 如何获取属性特征

    使用Object.getOwnPropertyDescriptor方法

    var descriptor = Object.getOwnPropertyDescriptor(person, 'name');
    console.log(descriptor.value + ' ' + descriptor.writable + ' ' + descriptor.enumerable);
    复制代码

建立对象

这一节,将会介绍多种建立对象的方法。

工厂模式

/// 工厂模式
/// 这种模式减小了建立多个类似对象的重复代码,
/// 但没法解决对象识别问题(即怎样知道对象的类型)
function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayHi = function() {
        alert('Hi! I\'m ' + this.name); } return o; } var instance = createPerson('GY', 18); person.sayHi(); 复制代码

构造函数模式

/// 构造函数模式
/// 这种方式须要显示的使用 new 关键字
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHi = function() {
        alert('Hi! I\'m ' + this.name); }; } var instance = new Person('GY1', 18); person.sayHi(); 复制代码

这里不像工厂模式那样直接建立对象,但最终结果相同。主要由于使用new关键字会经历下面几个步骤:

  • 建立对象
  • 将构造函数的做用域赋值给新对象(所以this就指向了该新对象)
  • 执行构造函数中的代码
  • 返回新对象

这里能够经过person.constructor === Person明确知道其类型。使用instanceof检测也是经过的。

alert(instance.constructor === Person); // true
alert(instance instanceof Person); // true
复制代码
  • 构造函数也是函数,能够不经过new直接使用

    任何函数经过new来调用,均可以做为构造函数;任何函数,不经过new调用,那它跟普通函数也没什么两样。

    因为构造函数中使用了this,不经过new来使用,this将指向global对象(浏览器中就是window)。

    // 这样直接调用会在window对象上定义name和age属性
    Person('Temp', 10);
    复制代码
  • 构造函数也存在问题

    使用构造函数的主要问题是,每个方法都会在每一个对象上从新定义一遍。每一个对象上的方法都是不相等的。这样会形成内存浪费以及不一样的做用域链和标识符解析。很明显,这样是不必的。

    咱们可使用下面的方法来避免:

    function sayHi() {
        alert(this.name);
    }
    
    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayHi = sayHi;
    }
    
    var instance1 = new Person('instance1', 18);
    var instance2 = new Person('instance2', 20);
    复制代码

    就是把每一个方法都单独定义,在构造函数内部引用。这样又引起了新的问题:全局域上定义的函数实际为某些对象而服务,这样全局域有点名存实亡。其次,若是对象上须要有不少方法,那么这些方法都须要在全局域上定义。

    再来看看下面生成对象的方法。

原型模式

每一个函数都有一个prototype(原型)属性,这是一个指针,指向一个对象,该对象是用来包含特定类型全部实例共享的属性和方法的。 这样,以前在构造函数中定义的实例信息就能够写在原型对象中了。以下:

function Person() {
}

Person.prototype.name = 'unnamed';
Person.prototype.age = 18;
Person.prototype.sayHi = function() {
    alert(this.name);
}

var instance1 = new Person();
alert(instance1.name);
var instance2 = new Person();
alert(instance2.name);
复制代码

继续往下以前,先来了解下原型对象:

不管何时,只要建立了一个新函数,就会根据特定规则为该函数建立prototype属性,这个属性指向函数的原型对象。默认状况下,该原型对象还会拥有constructor(构造函数)属性,指向该函数。固然,也包含从Object对象继承来的属性(这个咱们后面再讲)。

在经过构造函数建立新实例对象后,每一个实例对象能够经过__proto__来访问构造函数的原型对象。

下面是他们的关系图:

咱们可使用Person.prototype.isPrototypeOf(instance1)来检测一个对象(这里为Person的prototype对象)是否为指定对象(这里为instance1)的原型。

推荐使用Object.getPrototypeOf(instance1)获取指定对象的原型对象,而不是使用__proto__

上面的例子中,实例对象中均没有name属性,却可以访问到。也正是由于原型对象的缘由:当代码获取某个对象属性时,都会执行一次搜索,目标是具备给定名字的属性。搜索首先从对象实例自己开始,若是找到对应属性,返回该值;若是没有找到,继续搜索原型对象。

从搜索过程能够发现,实例对象和原型对象都有的属性,实例中的会覆盖原型中的。使用delete删除时,只是删除了实例中的属性。

使用hasOwnProperty方法能够检测一个属性是否在实例中。只有给定属性存在于实例中,才会返回true。

使用in操做符时(property in object),无论是在实例中,仍是原型中都会返回true。

使用for-in操做时,返回的是全部可以经过对象访问的、可枚举的属性,无论是在实例中,仍是原型中;

既然原型对象也是对象,那咱们能够手动赋值原型对象,从而减小没必要要的输入。向下面这样:

function Person() {
}

Person.prototype = {
    constructor: Person, // constructor记得要声明
    name: 'unnamed',
    age: 0,
    sayHi: function() {
        alert('Hi! this is ' + this.name);
    }
};
复制代码

注意:原生的constructor是不可枚举的,这样定义后,致使constructor也能够枚举。

因为在原型中查找值是一次搜索过程,这就致使了原型的动态性。也就是说咱们对原型对象的操做都会马上反应在实例对象上。可是,若是咱们从新赋值了构造函数的原型对象,那在赋值以前建立的对象将不受影响,缘由是以前的实例对象指向的原型和如今的原型是彻底不一样的两个对象了。

到这里,你也许就能理解原生对象的方法都存储在其原型对象上了吧。

那么使用原型建立对象的方法是否是没有问题了呢?答案是否认的。首先,其省略了构造函数传参的环节,结果就是全部实例默认会有相同的属性值;其次,因为共享属性,在属性值为引用类型时,一个实例修改属性会影响另外一个实例。

function Person() {
}

Person.prototype = {
    constructor: Person, // constructor记得要声明
    name: 'unnamed',
    age: 0,
    sayHi: function() {
        alert('Hi! this is ' + this.name);
    },
    friends: ['A', 'B'] // 增长了friends属性
};

var instance1 = new Person();
var instance2 = new Person();

instance1.friends.push('C'); // 修改instance1的friends属性
alert(instance2.friends); // 但instance2的friends属性一样也改变成了A, B, C
复制代码

组合使用构造函数模式和原型模式

该模式使用构造函数定义实例属性,使用原型定义共享的方法和属性。

/// 定义实例属性
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friends = [];
}

/// 定义公共方法
Person.prototype = {
    constructor: Person, // constructor记得要声明
    sayHi: function() {
        alert('Hi! this is ' + this.name);
    }
};

var instance1 = new Person('instance1', 18);
var instance2 = new Person('instance2', 20);
复制代码

这种使用构造函数与原型混合的模式,是目前ECMAScript使用最普遍、认同度最高的建立自定义类型的方法。

动态原型

也许你对上面的组合模式将属性和方法分开来写的形式感到别扭。那么动态原型模式能够来拯救你。

动态原型模式,将组合模式中分开的代码合并在一块儿,经过判断动态的添加共享的方法或属性。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friends = [];

    // 这里就是动态原型模式和混合模式的区别
    // 对于共享的属性或方法,你没必要每个都去判断
    // 找到一个标准就行
    if (typeof this.sayHi != 'function') {
        Person.prototype.sayHi = function() {
            alert('Hi! I\'m ' + this.name); }; } } var instance1 = new Person('instance1', 18); instance1.sayHi(); 复制代码

注意:这里不能使用字面量语法重写原型属性,这样会切断以前建立的实例和现有原型的联系。

寄生构造函数模式

寄生构造函数模式提供一个函数用于封装建立对象的代码.这和工厂模式极为类似。

function Person(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayHi = function() {
        alert('Hi! I\'m ' + this.name); }; return o; } var instance = new Person('instance', 18); instance.sayHi(); 复制代码

既然函数内部并无使用new操做生成的实例对象,为啥还要生成?这个点暂时没搞懂。

能够看到,和工厂模式相比,除了使用new操做符觉得,其余的没啥两样。可是在一些特定场合,仍是有它的用武之地的。好比ES6以前原生类型是没法继承的,可使用这种方法生成继承原生类型的实例

看下这个例子:

function SpecialArray() {
    var values = new Array();
    values.push.apply(values, arguments);
    // 提供新方法
    values.toPipedString = function() {
        return this.join('|');
    };
    
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
alert(colors.toPipedString());
复制代码

这里提供了构造函数,内部使用Array对象,并添加了特有方法。

之因此叫作寄生,缘由大概由于新的功能依托于原有对象吧。

稳妥构造函数模式

所谓稳妥,指没有公共属性,并且其方法不引用this。在该模式中不适用new来调用构造函数。下面是个例子:

function Person(name, age) {
    var o = new Object();
    o.sayHi = function() {
        alert('Hi! I\'m ' + name); } return o; } var instance = Person('instance', 18); instance.sayHi(); 复制代码

也许你会纳闷,这里的name没有显示的存储,到底如何能访问到?请看下面的断点截图。说明了其存储在闭包中,或者说被闭包捕获了。(若理解有误请告知。谢谢!)

继承

ECMAScript中依靠原型链来实现继承。

原型链

简单回顾下构造函数、原型、和实例之间的关系:构造函数也是对象,该对象拥有prototype属性,指向了其原型对象,原型对象存在一个constructor属性,指向了该构造函数;实例对象拥有__proto__属性,也指向了构造函数的原型对象(再次说明下,__proto__不推荐使用哈,)。

如今,若是让构造函数的prototype属性指向另外一个类型的实例对象呢?上面的状况会层层递进。让咱们看下面的例子:

// 父类型
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

// 子类型
function SubType() {
    this.subproperty = false;
}

// 子类型的prototype指向父类型的实例对象
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue()); // true
复制代码

对应关系图:

值得注意的是,instance.constructor将获得的是SuperType。

  • 默认的原型

    全部引用类型都继承了Object,也就是说全部函数的prototype指向了Object实例。让咱们更新下上面的关系图:

  • 肯定原型和实例的关系 可使用instanceofisPrototypeOf方法

    alert(instance instanceof Object);
    alert(instance instanceof SuperType);
    alert(instance instanceof SubType);
    alert(Object.prototype.isPrototypeOf(instance));
    alert(SuperType.prototype.isPrototypeOf(instance));
    alert(SubType.prototype.isPrototypeOf(instance));
    复制代码

    这里都会返回true。判断规则为:实例的原型在原型链中。咱们可使用下面的方法进行模拟。

    /// 对象是不是指定类型的实例,会查找原型链
    Object.prototype.isKindsOf = function(func) {
        for (
            let proto = Object.getPrototypeOf(this); 
            proto !== null; 
            proto = Object.getPrototypeOf(proto)
        ) {
            if (proto === func.prototype) {
                return true
            }
        }
        return false
    }
    
    /// 对象是不是指定类型的实例,不进行原型链判断
    Object.prototype.isMemberOf = function(func) {
        return Object.getPrototypeOf(this) === func.prototype
    }
    复制代码
  • 原型链存在的问题

    经过原型实现继承,是将子类的原型对象赋值为父类(这里暂时使用子类和父类来表述)的实例,这样,原先父类的实例属性成了子类原型属性,会被子类的全部实例共享,这也包含引用类型的属性。

    再者,建立子类类型实例时,没法向父类的构造函数中传递参数。

    下面咱们一块儿看看如何解决这些问题。

借用构造函数

// 父类型
function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

// 子类型
function SubType() {
    // 使父类的构造函数在子类实例对象上初始化
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push('gray');
var instance2 = new SubType();
alert(instance2.colors); // red, blue, green
复制代码

这里在构造子类实例时,调用父类构造函数,完成父类特定的初始化。 像下面这样,还能够完成参数的传递。

// 父类型
function SuperType(name) {
    this.name = name;
}

// 子类型
function SubType() {
    // 使父类的构造函数在子类实例对象上初始化
    // 这样子类的属性就会覆盖其原型属性
    SuperType.call(this, 'unnamed');
}

var instance = new SubType();
alert(instance.name); // unnamed
复制代码

借用构造函数也存在一些问题。好比,方法没法实现复用;父类原型中定义的方法对子类不可见(由于这种情形子类和父类并无原型链上的关系,只是子类在构造过程当中借用了父类的构造过程)。

组合继承

将原型链和借用构造函数组合一块儿,使用原型链实现对原型属性和方法的继承,借用构造函数实现实例属性的继承。这成为最经常使用的继承模式。下面是一个例子:

// 父类型
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayHi = function() {
    alert('Hi! I\'m ' + this.name);
};

// 子类型
function SubType(name, age) {
    // 使父类的构造函数在子类实例对象上初始化, 完成实例属性的继承
    SuperType.call(this, name);
    // 子类特有的属性
    this.age = age;
}

// 子类型的prototype指向父类型的实例对象,完成继承
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){ 
    alert(this.name + ' is ' + this.age + ' years old!');
}

var instance1 = new SubType('instance1', 18);
instance1.colors.push('black');
alert(instance1.colors); // red,blue,green,black
instance1.sayHi(); // Hi! I'm instance1
instance1.sayAge(); // instance1 is 18 years old!

var instance2 = new SubType('instance2', 20);
alert(instance2.colors); // red,blue,green
instance2.sayHi(); // Hi! I'm instance2
instance2.sayAge(); // instance2 is 20 years old!
复制代码

原型式继承

这是一种借助已有对象建立新对象,同时没必要建立自定义类型的方法。先看下面的例子:

function object(o) {
    /// 建立临时构造函数
    function F(){}
    /// 将构造函数的原型赋值为传入的对象
    F.prototype = o;

    /// 返回实例
    return new F();
}

var person = {
    name: 'instance1',
    friends: ['gouzi', 'maozi']
};

var anotherPerson = object(person);
anotherPerson.name = 'cuihua';
anotherPerson.friends.push('xiaofeng');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'daha';
yetAnotherPerson.friends.push('bob');

alert(person.friends);
复制代码

能够看到,这里至关于复制了person的两个副本。在ECMAScript5中,新增了Object.create方法来规范了原型继承模式。

/// 能够只传入一个参数
var anotherPerson = Object.create(person);

// 也可多传入属性及其特性
var yetAnotherPerson = Object.create(person, {
    name: {
        value: 'dab'
    }
});
复制代码

寄生式继承

相比原型继承,寄生式继承仅是提供一个函数,用来封装对象的继承过程。以下:

function createAnother(original) {
    /// 向原型继承同样,建立新对象
    var clone = Object.create(original)
    /// 自定义的加强过程
    clone.sayHi = function() {
        alert('Hi!');
    }
    return clone;
}
复制代码

能够发现,该模式,没法对函数进行复用。

寄生组合模式

回顾下以前的组合继承方式,这会致使两次父类构造函数调用:一次在建立子类原型,另外一次在建立子类实例。这将致使,子类的实例和原型中都存在父类的属性。以下:

// 父类型
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayHi = function() {
    alert('Hi! I\'m ' + this.name);
};

// 子类型
function SubType(name, age) {
    // 使父类的构造函数在子类实例对象上初始化, 完成实例属性的继承
    SuperType.call(this, name); // 又一次父类构造函数调用
    // 子类特有的属性
    this.age = age;
}

// 子类型的prototype指向父类型的实例对象,完成继承
SubType.prototype = new SuperType(); // 一次父类构造函数调用
SubType.prototype.sayAge = function(){ 
    alert(this.name + ' is ' + this.age + ' years old!');
}

var instance = new SubType();
instance.colors.push('gray');
alert(instance.colors); // red,blue,green,gray
alert(instance.__proto__.colors); // red,blue,green
复制代码

为了解决这个问题,能够考虑使用构造函数继承属性,使用原型链来继承方法;没必要为了指定子类的原型而调用父类的构造函数(这样就避免了生成父类的实例,由于父类的原型对象已经存在了),咱们须要的就是原型对象的一个副本而已。看看下面的例子:

// 父类型
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayHi = function() {
    alert('Hi! I\'m ' + this.name);
};

// 子类型
function SubType(name, age) {
    // 使父类的构造函数在子类实例对象上初始化, 完成实例属性的继承
    SuperType.call(this, name); // 又一次父类构造函数调用
    // 子类特有的属性
    this.age = age;
}

/// 使用寄生的方式完成继承
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){ 
    alert(this.name + ' is ' + this.age + ' years old!');
}

function inheritPrototype(subType, superType) {
    // 得到父类原型副本,做为子类的原型
    var prototype = Object.create(superType.prototype);
    // 配置子类原型
    prototype.constructor = subType;
    // 指定子类原型
    subType.prototype = prototype;
}

var instance = new SubType();
instance.colors.push('gray');
alert(instance.colors); // red,blue,green,gray
alert(instance.__proto__.colors); // undefined
复制代码

至此,咱们找到了一种最理想的继承模式。

总结

该篇从对象的含义,到对象的建立方式,再到继承的多种实现。由浅入深的介绍了ECMAScript中面向对象相关知识。也许你在阅读过程当中感到疑惑,那么就动手实现一遍...那时,就没必要多说什么了!

参考

相关文章
相关标签/搜索