【读书笔记】JavaScript面向对象精要(下)

1,329 阅读17分钟

写在前面

上一篇文章中我们对书的前三章进行了总结整理~

点击这里获取上一篇文章:👉【读书笔记】JavaScript面向对象精要(上)

这篇文章将整理总结本书的最后三章内容,让我们彻底明白JavaScript的面向对象。下面是完整的思维导图:

第四章 构造函数和原型对象

4.1 构造函数

构造函数是我们用new创建对象时调用的函数,比如:ObjectArrayFunction等。构造函数同函数同样的定义方式,但是构造函数的首字母应该大写,像这样:

function Person({
    // .....
}

var person = new Person();     // 可传参的写法
或者
var person = new Person;       // 不用传参也可以这么写

// 检查person是否为Person类型(推荐使用instanceof的方式)
console.log(person instanceof Person);    // true

通过自动以构造函数创建出来的对象,其构造函数属性指向创建它的构造函数

console.log(person.constructor === Person);    // true

创建一个构造函数的目的是创建许多拥有相同属性和方法的对象。

  • this添加属性
function Person (name{
    this.name = name;
    this.sayName = function({
        return this.name;
    }
}

var person1 = new Person('熊大');
var person2 = new Person('熊二');

console.log(person1.name, person1.sayName());   // '熊大' '熊大'
console.log(person2.name, person2.sayName());   // '熊二' '熊二'
  • Object.defineProperty()方法
function Person(name{
    Object.defineProperty(this"name", {
        getfunction({
            return name;
        },
        setfunction(value{
            name = value;
        },
        // ...一些配置属性
    });
    this.sayName = function({
        return this.name;
    }
}

var person = new Person('熊大');

console.log(person.sayName());         // 熊大

构造函数并不会消除代码冗余。

4.2 原型对象

几乎所有的函数都有一个prototype属性,该属性是一个原型对象用来创建一个新的对象实例。所有创建的对象实例共享该原型对象,并且这些对象实例可以访问原型对象的属性。比如toString()方法

var obj = {
    name'熊大'
}

console.log("name" in obj);                                // true
console.log(obj.hasOwnProperty("name"));                   // true
console.log("toString" in obj);                            // true
console.log(obj.hasOwnProperty("toString"));               // false
console.log(Object.prototype.hasOwnProperty("toString"));  // true

我们在上一章中总结过:in操作符会检测自有属性和原型属性,而Object.hasOwnProperty()方法只会检测自有属性,所以我们可以按照这个思路写一个方法检测一个属性是否为原型属性:

/**
 * @params object {object} name {string}
 * @description 当是原型属性时返回true, 否则false
 * @returns boolean 
*/

function isProto(object, name) {
    return name in object && !object.hasOwnProperty(name);
}

var object = {
    name: '熊大'
}
console.log(isProto(object"name"));            // false
console.log(isProto(object"age"));             // false
console.log(isProto(object"toString"));        // true

4.2.1 [[Prototype]]属性

当我们用new创建一个新的对象时,构造函数的原型对象被赋给该对象的[[Prototype]]属性,我们用一张图感受一下多个对象实例引用同一个原型对象是怎么减少重复代码的


我们可以用Object.getPrototypeOf()方法读取[[Prototype]]属性的值,可以用isPrototypeOf()方法检查某个对象是否是另一个对象的原型对象

var obj = {};
var obj1 = {};

var val = Object.getPrototypeOf(obj);

console.log(val === Object.prototype);                 // true
console.log(Object.prototype.isPrototypeOf(obj));      // true
console.log(Object.prototype.isPrototypeOf(obj1));     // true

当我们读取一个对象的属性的时候,首先会查找该对象的自有属性,如果找到则返回值,如果没有找到,会接着查找对象的原型属性,如果找到则返回值,如果没有找到,最终返回undefined

var person = {
    name'熊大'
}

console.log(person.name);              // '熊大'
console.log(person.toString());        // [object object]
console.log(person.age);               // undefined

自有属性会覆盖原型属性,delete操作符仅对自有属性生效。

4.2.2 在构造函数中使用原型对象

因为原型对象的可以共享,所以我们可以一次性为所有对象定义方法。将方法放在原型对象中并用this访问是很好的一个方法。

function Person(name{
    this.name = name;
}

Person.prototype.sayName = function({
    return this.name;
}

var person = new Person('熊大');

console.log(person.name);          // '熊大'
console.log(person.sayName());     // '熊大'

由于原型对象的共享性,在原型上定义属性可能会出现以下情况

function Person(name{
    this.name = name;
}

Person.prototype.sayName = function({
    return this.name;
}

var person1 = new Person('熊大');
var person2 = new Person('熊二');

Person.prototype.arr = [];

person1.arr.push('前端');
person2.arr.push('Java');

console.log(person1.arr);      // 前端 Java
console.log(person2.arr);      // 前端 Java

所以,我们在原型上定义属性要慎重!!!!

我们可以直接用对象字面形式替换原型对象

function Person(name{
    this.name = name;
}

Person.prototype = {
    sayNamefunction({
        return this.name;
    },
    arr: [],
}

但是这样做会改变构造函数的属性,它会指向Object而不指向构造函数,

var person = new Person('熊大');

console.log(person instanceof Person);           // true         
console.log(person.constructor === Person);      // false
console.log(person.constructor === Object);      // true

constructor属性是Object特有的。所以为了避免出现这种情况,我们需要手动设置constructor属性

function Person(name{
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    sayNamefunction({
        return this.name
    }
}

var person = new Person('熊大');

console.log(person instanceof Person);           // true         
console.log(person.constructor === Person);      // true
console.log(person.constructor === Object);      // false

4.2.3 改变原型对象

我们在原型上添加一个属性,因为原型的共享性,当我们创建多个对象实例,这个属性是每个对象实例都拥有的。

还有一个有趣的现象,当我们封印或者冻结一个对象的时候,将无法添加或者改变自有属性,我们仍然可以通过原型对象扩展实例

4.2.4 内建对象的原型对象

所有的内建函数都有构造函数,我们也可以改变它的原型对象,如下例:

Array.prototype.sum = function({
    return this.reduce(function(prev, curr{
        return prev + curr
    })
}

var arr = [123456];

console.log(arr.sum());                  // 21

第五章 继承

5.1 原型对象链和Object.prototype

JS中内建的继承方法被称为原型对象链,又可称为原型对象继承。所有对象都继承自Object.prototype

5.1.1 继承自Object.prototype的方法

列举可被所有对象继承的方法

方法 作用
hasOwnProperty() 探测一个对象的自有属性是否存在
propertyIsEnumerable() 检查一个自有属性是否可枚举
isPropertyOf() 检查一个对象是否是另一个对象的原型对象
*valueOf() 返回一个对象的值表达
*toString() 返回一个对象的字符串表达

每当一个操作符被用于一个对象时就会调用valueOf方法,它会返回对象实例本身。而当valueOf返回的是一个引用而不是原始值的时候,就会调用toString()方法

5.1.2 修改Object.prototype

我们知道所有对象都继承自Object.prototype,如果改变,就会影响所有的对象,所以,我们最好不要修改Object.prototype()

5.2 对象继承

对象继承是最简单的继承类型,我们需要做的仅仅是指定哪个对象是新对象的[[prototype]],我们可以用对象字面形式隐式指定Object,也可以用Object.create()方法显式指定。

Object.create()方法的两个参数

  • 第一个参数:需要被设置为新对象的[[prototype]]对象
  • 第二个参数:可选参数。属性描述对象。
var person = {
    name'熊大'
}

var person = Object.create(Object.prototype, {
    name: {
        configurabletrue,
        enumerabletrue,
        value'熊大',
        writabletrue,
    }
})

上面两种声明方式是一样的结果,第一种方式自动继承Object.prototype,默认可配置、可枚举、可写。第二种方式自己定义继承哪个对象,接下来,用第二种方式继承另一个对象试试:

var person = {
    name'熊大',
    age11,
    sayNamefunction({
        return this.name;
    }
}

var person1 = Object.create(person, {
    name: {
        configurabletrue,
        enumerabletrue,
        value'熊二',
        writabletrue,
    }
})

console.log(person.sayName());                   // "熊大"
console.log(person1.sayName());                  // "熊二"

console.log(person.hasOwnProperty("age"));       // true
console.log(person.isPrototypeOf(person1));      // true
console.log(person1.hasOwnProperty("age"));      // false

我们也可以用Object.create()创建一个非常“干净”的对象

var obj = Object.create(null);

console.log("toString" in obj);     // false

5.3 构造函数继承

所有的函数都有prototype属性,它可以被修改或者被替换,该prototype属性被设置为一个新的继承自Object.prototype的泛用对象,该对象有一个自有属性constructor

function Func(){
    // ...
}

Func.prototype = Object.create(Object.prototype, {
    constructor: {
        configurabletrue,
        enumerabletrue,
        value: Func,
        writabletrue,
    }
})

我们还可以改变原型对象链

function Rectabgle(length, width{
    this.length = length;
    this.width = width;
}

Rectabgle.prototype.getArea = function({
    return this.length * this.width;
}

function Square(size){
    this.length = size;
    this.width = size;
}

Square.prototype = new Rectabgle();
Square.prototype.constructor = Square;

console.log(new Rectabgle(510).getArea());     // 50
console.log(new Square(6).getArea());            // 36

5.4 构造函数窃取

如果我们需要在子类构造函数中调用父类的构造函数,我们需要在构造函数中用call()apply()调用父类的构造函数,并将新的对象传进去,这个过程实际上就是在用自己的对象窃取父类的构造函数。

function Rectangle(length, width{
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function({
    return this.length * this.width;
}

function Square(size){
    Rectangle.call(this, size, size)
}

Square.prototype = new Rectangle();
Square.prototype.constructor = Square;

Square.prototype = Object.create(Rectangle.prototype, {
    name: {
        configurabletrue,
        enumerabletrue,
        value: Square,
        writabletrue,
    }
})

console.log(new Square(6).getArea());            // 36

上面的例子Square()方法调用了Rectangle构造函数,传入了thissize两次,这么做的目的是这样会在新对象上创建lengthwidth属性,这避免了在构造函数里重新定义你希望继承的属性手段,我们可以再调用完父类的构造函数后继续添加新属性或覆盖已有的属性,这通常被称为伪类继承。

5.5 访问父类方法

我们应该如何访问父类的方法,

function Person({
    this.name = '熊大'
}

Person.prototype.sayName = function ({
    return this.name;
}

function Person1({
    this.name = '熊二';
}

Person1.prototype.sayName = function({
    return Person.prototype.sayName.call(this);
}

var person = new Person();
var person1 = new Person1();
console.log(person1.sayName());              // '熊二'

这种方法是唯一的访问父类方法的手段

第六章 对象模式

6.1 私有成员和特权成员

JavaScript对象的所有属性都是公有的,并且没有显式的方法指定某个属性不能被外界对象访问。可能有些时候,我们希望这个属性是私有的,我们可以采用一种命名规则的方式解决这个问题,比如这样:this._name。还有其他的一些方法,下面我们一一列举

6.1.1 模块模式

它是一种用于创建私有数据的单间对象的模式。做法是使用立调函数(这种立调函数的语法是立刻执行匿名函数,这个函数仅存在于被调用的瞬间,调用后就被销毁)表达返回一个对象。

这种模块模式可以让我们使用普通的变量作为非公有的对象属性。

var obj = (function(){
    var name = '熊大';
    return {
        name,
        getNamefunction({
            return name;
        },
        setNamefunction({
            name = '熊二';
        }
    };
}());

console.log(obj.name);                   // "熊大"
console.log(obj.getName());              // "熊大"

obj.name = '熊二';
console.log(obj.name);                   // "熊二"
console.log(obj.getName());              // "熊大"

obj.setName();
console.log(obj.name);                   // "熊二"
console.log(obj.getName());              // "熊二"

上述代码中,name为私有属性,无法被外界直接访问,可以通过对象方法来操作。hetName()setName()为特权方法。

模块模式的一个变种:暴露模块模式。

var obj = (function({
    var name = '熊大';
    function getName({
        return name;
    }

    function setName({
        name = '熊二';
    }

    return {
        name,
        getName,
        setName,
    }
}())

这个暴露模块模式,属性namegetNamesetName都是IIFE的本地对象,她们都设置到了返回的对象中,向外界暴露了它自己。

6.1.2 构造函数的私有成员

在构造函数中同样也可以使用模块模式定义私有属性

function Person(name){
    this.name = name;

    var age = 11;

    this.getAge = function({
        return age;
    }

    this.setAge = function({
        return age = 22;
    }
}

var person = new Person('熊大');

console.log(person.name);              // '熊大'
console.log(person.getAge());          // 11

person.age = 12;
console.log(person.getAge());         // 11

person.setAge();
console.log(person.getAge());         // 22

如果,我们需要所有实例可共享私有数据,可结合模块模式和构造函数

var PersonFather = (function({
    var age = 11;

    function Person(name{
        this.name = name;
    }

    Person.prototype.getAge = function({
        return age;
    }

    Person.prototype.setAge = function({
        age =  22;
    }

    return Person;
}())

var person =  new PersonFather('熊大');
var person1 = new PersonFather('熊二');

console.log(person.name);                  // '熊大'
console.log(person.getAge());              // 11
console.log(person1.getAge());             // 11

person.age = 22;
console.log(person.getAge());              // 11
console.log(person1.getAge());             // 11

person.setAge();
console.log(person.getAge());              // 22
console.log(person1.getAge());             // 22

上述,PersonFather函数的全部实例成功共享age变量。

6.2 混入

混入是一种伪继承的手段,它是指一个对象在不改变原型对象链的情况下得到了另一个对象的属性。

传统函数实现

// 浅拷贝
function mixin(receiver, supplier{
    for(var property in supplier) {
        if(object.hasOwnProperty(property)) {
            receiver[property] = supplier[property]
        }
    }
    return receiver;
}

我们可以写出一个函数让其支持ECMAScript5ECMAScript3的版本

function mixin(receiver, supplier{
    if(Object.getOwnPropertyDescriptor) {
        Object.keys(supplier).forEach(function(property{
            var descriptor = Object.getOwnPropertyDescriptor(supplier, property);
            Object.defineProperty(receiver, property, descriptor);
        })
    } else {
        for(var property in supplier) {
            if(object.hasOwnProperty(property)) {
                receiver[property] = supplier[property]
            }
        }
    }
    return receiver;
}

6.3 作用域安全的构造函数

当我们使用构造函数的时候,如果不用new操作符调用,就会抛出一个错误,下面是一个作用域安全的构造函数写法

function Person(name{
    if(this instanceof Person) {
        this.name = name
    } else {
        return new Person(name)
    }
}

// 这样之后,我们不用new操作符,也不会抛出错误
var person = new Person('熊大');
var person1 = Person('熊二');

console.log(person instanceof Person);       // true
console.log(person1 instanceof Person);      // true

推荐阅读

【JavaScript系列】带你手写实现new运算符

总结

至此,《JavaScript面向对象精要》这本书已经总结完毕,有总结的不好的地方,还希望各位指点出来,让我们共同进步~

最后,分享一下我的个人公众号「web前端日记」,欢迎大家前来关注~