美文网首页
Javascript 面向对象精要

Javascript 面向对象精要

作者: 王国的荣耀 | 来源:发表于2020-08-05 16:40 被阅读0次
    image.png

    中文名:Javascript 面向对象精要
    英文名:The Principles of Object-Oriented JavaScript

    这本书是好多年前的写的js的面向对象的技术点,写的非常好。
    书很薄,英文版只有120多页,汉语版有将近90页。

    术语
    IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。

    数据类型

    在JavaScript中,数据类型分为两类:

    原始类型

    保存一些简单数据,如true,5等。JavaScript共有5中原始类型:

    类型 描述
    boolean 布尔,值为true或false
    number 数字,值为任何整型会浮点数值
    string 字符串,值为由单引号或双引号括出的单个字符或连续字符(JavaScript不区分字符类型)
    null 空类型,其仅有一个值:nulll
    undefined 未定义,其仅有一个值:undefined
    // strings
    var name = "Nicholas"; var selection = "a";
    // numbers
    var count = 25; var cost = 1.51;
    // boolean
    var found = true; // null
    var object = null;
    // undefined
    var flag = undefined;
    var ref; // assigned undefined automatically
    

    原始类型的值是直接保存在变量中,并可以用 typeof 进行检测。但是typeof对null的检测是返回object,而不是返回null,所以检测null时,最好用全等于(===),其还能避免强制类型转换。

    console.log(typeof "Nicholas"); // "string"
    console.log(typeof 10); // "number"
    console.log(typeof 5.1); // "number"
    console.log(typeof true); // "boolean"
    console.log(typeof undefined); // "undefined"
    
    console.log(typeof null); // "object"
    //判断一个值是否为空类型的最佳方法是直接和null比较,如下。
    console.log(value === null); // true or false
    
    

    引用类型

    引用类型
    保存为对象,实质是指向内存位置的引用,所以不在变量中保存对象。除了自定义的对象,JavaScript提供了6中内建类型:
    Array:数组类型,以数字为索引的一组值的有序列表
    Date:日期和时间类型
    Error:运行期错误类型
    Function:函数类型
    Object:通用对象类型
    RegExp:正则表达式类型

    var object = new Object();
    var object1 = new Object();
    var object2 = object1;
    

    鉴别引用类型
    instanceof操作符以一个对象和一个构造函数为参数。如果对象是构造函数所指定的类型的一个实例,instanceof返回true;否则返回false,如下例。

    var items = [];
    var object = {};
    function reflect(value) {
    return value;
    }
    console.log(items instanceof Array); // true
    console.log(object instanceof Object); // true
    console.log(reflect instanceof Function); // true
    

    鉴别数组
    ECMAScript 5引入了Array.isArray()来明确鉴别一个值是否为Array的实例,无论该值来自哪里,该方法对来自任何上下文的数组都返回true。

    var items = [];
    console.log(Array.isArray(items)); // true
    

    typeof 与instanceof的区别

    var name = new String("Nicholas");
    var count = new Number(10);
    var found = new Boolean(false);
    console.log(typeof name); // "object"
    console.log(typeof count); // "object"
    console.log(typeof found); // "object"
    

    手动创建原始封装类型实际会创建出一个object,这意味着typeof无法鉴别出你实际保存的数据的类型。

    函数

    var result = add(5, 5);
    //函数声明
    function add(num1, num2) {
    return num1 + num2;
    }
    
    //函数表达式:匿名函数
    var add = function(num1, num2) {
    return num1 + num2;
    };
    

    函数声明和函数表达式的区别:函数声明会被提升至上下文(要么是该函数被声明时所在的函数的范围,要么是全局范围)的顶部。这意味着你可以先使用函数后声明它们。

    函数就是对象

    函数是JavaScript的一大重点,你可以像使用对象一样使用函数。也可以将它们赋给变量,在对象中添加它们,将它们当成参数传递给别的函数,或从别的函数中返回。

    function sayHi() {
    console.log("Hi!");
    }
    sayHi(); // outputs "Hi!"
    var sayHi2 = sayHi;
    sayHi2(); // outputs "Hi!"
    
    var sayHi = new Function("console.log(\"Hi!\");");
    sayHi(); // outputs "Hi!"
    var sayHi2 = sayHi;
    sayHi2(); // outputs "Hi!"
    
    

    Function构造函数更加清楚地表明sayHi能够像其他对象一样被传来传去。只要你记住函数就是对象。

    函数的参数

    arguments对象不是一个数组的实例,其拥有的方法与数组不同,Array.isArray(arguments)永远返回false。

    function sum() {
    //   console.log(Array.isArray(arguments)); // false
    //   console.log(typeof arguments); // false
      var result = 0,
        i = 0,
        len = arguments.length;
      while (i < len) {
        result += arguments[i];
        i++;
      }
      return result;
    }
    //3
    console.log(sum(1, 2));  
    // 18 
    console.log(sum(3, 4, 5, 6));
    // 50
    console.log(sum(50));
    //0
    console.log(sum()); 
    

    函数的重载

    JavaScript里,当你试图定义多个同名的函数时,只有最后定义的有效,之前的函数声明被完全删除,只使用最后那个。

    JavaScript函数没有签名这个事实不意味着你不能模仿函数重载。你可以用arguments对象获取传入的参数个数并决定怎么处理。例如,

    function sayMessage(message) {
    if (arguments.length === 0) {
    message = "Default message";
    }
    console.log(message);
    }
    sayMessage("Hello!"); // outputs "Hello!"
    

    this 的由来

    var person = {
    name:"Nicholas",
    sayName:function() {
    console.log(person.name);
    }
    };
    person.sayName(); // outputs "Nicholas"
    

    上面的代码使用太费劲了:sayName()方法直接引用了person.name,在方法和对象间建立了紧耦合。
    JavaScript所有的函数作用域内都有一个this对象代表调用该函数的对象。在全局作用域中,this代表全局对象(浏览器里的window)。当一个函数作为对象的方法被调用时,默认this的值等于那个对象。所以你应该在方法内引用this而不是直接引用一个对象。前例代码可以改写如下。

    var person = {
    name:"Nicholas",
    sayName:function() {
    console.log(this.name);
    }
    };
    person.sayName(); // outputs "Nicholas"
    

    函数中的this

    JavaScript提供了三个方法用于改变this的指向:call、apply和bind。三个函数的第一个参数都是指定this的值,其他参数均作为参数传递给函数。

    第一个用于操作this的函数方法是call(),它以指定的this值和参数来执行函数。call()的第一个参数指定了函数执行时this的值,其后的所有参数都是需要被传入函数的参数。假设你更新sayNameForAll让它接受一个参数,代码如下。

    function sayNameForAll(label) {
    console.log(label + ":" + this.name);
    }
    var person1 = {
    name:"Nicholas"
    };
    var person2 = {
    name:"Greg"
    };
    var name = "Michael";
    sayNameForAll.call(this, "global"); // outputs "global:Michael"
    sayNameForAll.call(person1, "person1"); // outputs "person1:Nicholas"
    sayNameForAll.call(person2, "person2"); // outputs "person2:Greg"
    

    apply只接受两个参数:this的值和一个数组或者类似数组的对象,内含需要被传入函数的参数(也就是说你可以把arguments对象作为apply()的第二个参数)。

    function sayNameForAll(label) {
    console.log(label + ":" + this.name);
    }
    var person1 = {
    name:"Nicholas"
    };
    var person2 = {
    name:"Greg"
    };
    var name = "Michael";
    sayNameForAll.apply(this, ["global"]); // outputs "global:Michael"
    sayNameForAll.apply(person1, ["person1"]); // outputs "person1:Nicholas"
    sayNameForAll.apply(person2, ["person2"]); // outputs "person2:Greg"
    

    bind()的第一个参数是要传给新函数的this的值。

    function sayNameForAll(label) {
    console.log(label + ":" + this.name);
    }
    
    var person1 = {
    name:"Nicholas"
    };
    
    var person2 = {
    name:"Greg"
    };
    
    // create a function just for person1
    var sayNameForPerson1 = sayNameForAll.bind(person1);
    
    sayNameForPerson1("person1");
     // outputs "person1:Nicholas"
    
    // create a function just for person2
    var sayNameForPerson2 = sayNameForAll.bind(person2, "person2");
    sayNameForPerson2(); // outputs "person2:Greg"
    
    // attaching a method to an object doesn't change 'this'
    person2.sayName = sayNameForPerson1;
    person2.sayName("person2"); // outputs "person2:Nicholas"
    

    理解对象

    对象是一种引用类型,创建对象常见的两种方式:Object构造函数和对象字面量形式:

    var person1 = {
    name:"Nicholas"
    };
    var person2 = new Object();
    person2.name = "Nicholas";
    person1.age = "Redacted";
    person2.age = "Redacted";
    person1.name = "Greg";
    person2.name = "Michael";
    

    属性探测

    在大多数情况下,in操作符是探测对象中属性是否存在的最好的途径。它还有一个额外的好处就是不会评估属性的值。
    delete操作符针对单个对象属性调用名为[[Delete]]的内部方法。

    var person1 = {
    name:"Nicholas",
    sayName:function() {
    console.log(this.name);
    }
    };
    console.log("name" in person1); // true
    console.log(person1.hasOwnProperty("name")); // true
    console.log("toString" in person1); // true
    console.log(person1.hasOwnProperty("toString")); // false
    
    delete person1.name; // true - not output
    console.log("name" in person1); // false
    console.log(person1.name); // undefined
    
    

    属性枚举

    所有你添加的属性默认都是可枚举的,也就是说你可以用for-in循环遍历它们。可枚举属性的内部特征[[Enumerable]]都被设置为true。

    var properties = Object.keys(object);
    // if you want to mimic for-in behavior
    var i, len;
    for(i=0, len=properties.length; i<len; i++){
    console.log("Name:" + properties[i]);
    console.log("Value:" + object[properties[i]]);
    } 
    

    并不是所有的属性都是可枚举的。
    实际上,对象的大部分原生方法的[[Enumerable]]特征都被置为false。你可以用propertyIsEnumerable()方法检查一个属性是否为可枚举的。每个对象都拥有该方法。

    var person1 = {
    name:"Nicholas"
    };
    console.log("name" in person1); // true
    console.log(person1.propertyIsEnumerable("name")); // true
    var properties = Object.keys(person1);
    console.log("length" in properties); // true
    console.log(properties.propertyIsEnumerable("length")); // false
    

    属性特征

    有两个属性特征是数据和访问器属性都具有的。一个是[[Enumerable]],决定了你是否可以遍历该属性。另一个是[[Configurable]],决定了该属性是否可配置。

    var person1 = {
    name:"Nicholas"
    };
    
    Object.defineProperty(person1, "name", {enumerable:false});
    
    console.log("name" in person1); // true
    console.log(person1.name); // true
    console.log(person1.propertyIsEnumerable("name")); // false
    
    var properties = Object.keys(person1);
    console.log(properties.length); // 0
    Object.defineProperty(person1, "name", {configurable:false});
    
    // try to delete the Property
    try {
      delete person1.name;
    } catch (error) {
      console.log('error ' + error);
    }
    
    console.log("name" in person1); // true
    console.log(person1.name); // "Nicholas"
    
    // happened error!!!
    //Object.defineProperty(person1, "name", {configurable:true});
    

    本例如通常一样定义了name属性,然后设置它的[[Enumerable]]特征为false。基于这个新值的propertyIsEnumerable()方法将返回false。之后name被改为不可配置。从现在起,由于该属性不能被改变,试图删除name将会失败,所以name依然存在于person1中。对name再次调用Object.defineProperty()也不会改变属性。person1对象的属性name被有效地锁定。
    最后几行代码试图重新定义name为可配置的。然而这将抛出错误。你无法将一个不可配置的属性变成可配置。同样,在不可配置的情况下试图将数据属性变为访问器属性或反向变更也会抛出错误。

    数据属性特征

    数据属性额外拥有两个访问器属性不具备的特征。第一个是[[Value]],包含属性的值。当你在对象上创建属性时该特征被自动赋值。所有的属性的值都保存在[[Value]]中,哪怕该值是一个函数。
    第二个特征是[[Writable]],该特征是一个布尔值,指示该属性是否可以写入。所有的属性默认都是可写的,除非你另外指定。

    通过这两个额外特征,你可以使用Object.defineProperty()完整定义一个数据属性,即使该属性还不存在。考虑下例代码。

    var person1 = {};
    Object.defineProperty(person1, "name", {
    value:"Nicholas",
    enumerable:true,
    configurable:true,
    writable:true
    });
    
    

    当你用Object.defineProperty()定义新的属性时一定记得为所有的特征指定一个值,否则布尔型的特征会被默认设置为false。

    设置[[Enumerable]]和[[Configurable]]可以改变访问器属性的工作方式。

    _name:"Nicholas"
    };
    Object.defineProperty(person1, "name", {
    get:function() {
    console.log("Reading name");
    return this._name;
    }
    });
    console.log("name" in person1); // true
    console.log(person1.propertyIsEnumerable("name")); // false
    delete person1.name;
    console.log("name" in person1); // true
    person1.name = "Greg";
    console.log(person1.name); // "Nicholas"
    

    在这段代码中,name属性是一个只有getter的访问器属性。没有setter,也没有任何特征被显式指定为true,所以它的值只能被读取,不能被改变。

    获取属性特征

    如果需要获取属性的特征,在JavaScript中可以用Object.get OwnPropertyDescriptor()方法。

    var person1 = {
    name:"Nicholas"
    };
    var descriptor = Object.getOwnPropertyDescriptor(person1, "name");
    console.log(descriptor.enumerable); // true
    console.log(descriptor.configurable); // true
    console.log(descriptor.writable); // true
    console.log(descriptor.value); // "Nicholas"
    

    这里,属性name作为对象字面形式的一部分被定义。调用Object.getOwnPropertyDescriptor()方法返回的属性描述对象具有4个属性:enumerable、configurable、writable和value,即使它们从没有被Object.defineProperty()显式定义。

    禁止修改对象的三种方法

    preventExtensions

    第一种方法是用Object.preventExtensions()创建一个不可扩展的对象。该方法接受一个参数,就是你希望使其不可扩展的对象。一旦在一个对象上使用该方法,就永远不能再给它添加新的属性了。可以用Object.isExtensible()来检查[[Extensible]]的值。请看下面代码。

    var person1 = {
    name:"Nicholas"
    };
    console.log(Object.isExtensible(person1)); // true
    Object.preventExtensions(person1);
    console.log(Object.isExtensible(person1)); // false
    person1.sayName = function() {
    console.log(this.name);
    };
    console.log("sayName" in person1); // false
    

    创建person1后,这段代码先检查其[[Extensible]]特征,然后将其变得不可扩展。由于不可扩展,sayName()方法永远无法被加到person1上。

    对象封印

    对象封印是创建不可扩展对象的第二种方法。一个被封印的对象是不可扩展的且其所有属性都不可配置。这意味着不仅不能给对象添加新属性,也不能删除属性或改变其类型(从数据属性变成访问器属性或相反)。如果一个对象被封印,只能读写它的属性。

    可以用Object.seal()方法来封印一个对象。该方法被调用时,[[Extensible]]特征被置为false,其所有属性的[[Configurable]]特征被置为false。如

    下面所示,可以用Object.isSealed()判断一个对象是否被封印。

    var person1 = {
    name:"Nicholas"
    };
    
    console.log(Object.isExtensible(person1)); // true
    console.log(Object.isSealed(person1)); // false
    Object.seal(person1);
    console.log(Object.isExtensible(person1)); // false
    console.log(Object.isSealed(person1)); // true
    person1.sayName = function() {
    console.log(this.name);
    };
    console.log("sayName" in person1); // false
    person1.name = "Greg";
    console.log(person1.name); // "Greg"
    delete person1.name;
    console.log("name" in person1); // true
    console.log(person1.name); // "Greg"
    var descriptor = Object.getOwnPropertyDescriptor(person1, "name");
    console.log(descriptor.configurable); // false
    

    这段代码封印了person1,因此不能在person1上添加或删除属性。所有的被封印对象都是不可扩展的对象,此时对person1使用Object.isExtensible()方法将会返回false,且试图添加sayName()会失败。而且,虽然person1.name被成功改变为一个新值,但是删除它将会失败。

    对象冻结

    创建不可扩展对象的最后一种方法是冻结它。如果一个对象被冻结,则不能在其上添加或删除属性,不能改变属性类型,也不能写入任何数据属性。简而言之,被冻结对象是一个数据属性都为只读的被封印对象。被冻结对象无法解冻。可以用Object.freeze()来冻结一个对象,用Object.isFrozen()来判断一个对象是否被冻结,代码如下。

    var person1 = {
    name:"Nicholas"
    };
    
    console.log(Object.isExtensible(person1)); // true
    console.log(Object.isSealed(person1)); // false
    console.log(Object.isFrozen(person1)); // false
    Object.freeze(person1);
    console.log(Object.isExtensible(person1)); // false
    console.log(Object.isSealed(person1)); // true
    console.log(Object.isFrozen(person1)); // true
    
    person1.sayName = function() {
    console.log(this.name);
    };
    
    console.log("sayName" in person1); // false
    person1.name = "Greg";
    console.log(person1.name); // "Nicholas"
    delete person1.name;
    console.log("name" in person1); // true
    console.log(person1.name); // "Nicholas"
    var descriptor = Object.getOwnPropertyDescriptor(person1, "name");
    console.log(descriptor.configurable); // false
    console.log(descriptor.writable); // false
    

    在本例中,person1被冻结。被冻结对象也被认为是不可扩展对象和被封印对象,所以Object.isExtensible()返回false,而Object.isSealed()则返回true。属性name无法被改变,所以试图对其赋值为“Greg”的操作失败,后续的检查依旧返回“Nicholas”。

    构造函数

    构造函数

    构造函数就是用new创建对象时调用的函数。使用构造函数的好处在于所有用同一个构造函数创建的对象都具有同样的属性和方法。

    function Person(){}
    var p1 = new Person();
    console.log(p1.constructor === Person); // true
    console.log(p1 instanceof Person);      // true
    

    可以使用构造函数来检查一个对象的类型,但还是建议使用instanceof来检查对象类型。因为构造函数属性可以被覆盖,并不一定完全准确。

    构造函数中显示调用return:
    如果返回的值是一个对象,它会替代新创建的对象实例返回;
    如果返回的值是一个原始类型,它会被忽略,新创建的对象实例会被返回。

    //构造函数返回对象
    function Foo(){
        this.name = "foo";
        return {name: "hhh"};
    }
    var f = new Foo();
    f.name;         // hhh
    f.constructor;  // Object
    
    //构造函数返回原始类型
    function Too(){
        this.name = "too";
        return "hhh";
    }
    var t = new Too();
    t.name;         // too
    t.constructor;  // Too
    
    

    当Person不是被new调用时,构造函数中的this对象等于全局this对象。由于Person构造函数依靠new提供返回值,person1变量为undefined。没有new,Person只不过是一个没有返回语句的函数。对this.name的赋值实际上是创建了一个全局变量name来保存传递给Person的参数。第6章介绍的几个对象模式中将包括对这一问题的解决方法。

    var person1 = Person("Nicholas"); // note:missing "new"
    console.log(person1 instanceofPerson); // false
    console.log(typeof person1); // "undefined"
    console.log(name); // "Nicholas"
    

    原型对象

    hasOwnProperty

    hasOwnProperty判断对象是否包含特定的自身(非继承)属性,判断自身属性是否存在。判断一个对象是否有名称的属性或对象,此方法无法检查该对象的原型链中是否具有该属性,该属性必须是对象本身的一个成员。
    如果该属性或者方法是该对象自身定义的而不是器原型链中定义的 则返回true;否则返回false;

    格式如下:
    object.hasOwnProperty(proName);
    判断proName的名称是不是object对象的一个属性或对象。

    var person1 = {
    name:"Nicholas",
    sayName:function() {
    console.log(this.name);
    }
    };
    var person2 = Object.create(person1, {
    name:{
    configurable:true,
    enumerable:true,
    value:"Greg",
    writable:true
    }
    });
    person1.sayName(); // outputs "Nicholas"
    person2.sayName(); // outputs "Greg"
    console.log(person1.hasOwnProperty("sayName")); // true
    console.log(person1.isPrototypeOf(person2)); // true
    console.log(person2.hasOwnProperty("sayName")); // false
    

    一个对象实例通过内部属性[[Prototype]]跟踪其原型对象。该属性是一个指向该实例使用的原型对象的指针。当你用new创建一个新的对象时,构造函数的原型对象会被赋给该对象的[[Prototype]]属性。下图显示了[[Prototype]]属性是如何让多个对象实例引用同一个原型对象来减少重复代码的。

    Prototype属性

    大部分JavaScript引擎在所有对象上都支持一个名为proto的属性。

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

    function Person(name) {
    this.name = name;
    }
    Person.prototype= {
    sayName:function() {
    console.log(this.name);
    },
    
    toString:function() {
    return "[Person " + this.name + "]";
    }
    };
    var person1 = new Person("Nicholas");
    console.log(person1 instanceof Person); // true
    console.log(person1.constructor === Person); // false
    console.log(person1.constructor === Object); // true
    

    上段代码在原型对象上定义了两个方法,sayName()和toString()。这种定义的方式非常流行,因为这种方式不需要多次键入Person.prototype。但是有一个副作用需要注意。

    使用对象字面形式改写原型对象改变了构造函数的属性,因此它现在指向Object而不是Person。这是因为原型对象具有一个constructor属性,这是其他对象实例所没有的。当一个函数被创建时,它的prototype属性也被创建,且该原型对象的constructor属性指向该函数。当使用对象字面形式改写原型对象Person.prototype时,其constructor属性将被置为泛用对象Object。

    image.png

    isPrototypeOf

    isPrototypeOf是用来判断指定对象object1是否存在于另一个对象object2的原型链中,是则返回true,否则返回false。

    格式如下:
    object1.isPrototypeOf(object2);
    object1是一个对象的实例;
    object2是另一个将要检查其原型链的对象。

    原型链可以用来在同一个对象类型的不同实例之间共享功能。
    如果 object2 的原型链中包含object1,那么 isPrototypeOf 方法返回 true。
    如果 object2 不是一个对象或者 object1 没有出现在 object2 中的原型链中,isPrototypeOf 方法将返回 false。

    改变原型对象

    [[Prototype]]属性只是包含了一个指向原型对象的指针,任何对原型对象的改变都立即反映到所有引用它的对象实例上。

    function Person(name) {
    this.name = name;
    }
    
    Person.prototype= {
    constructor:Person,
    sayName:function() {
    console.log(this.name);
    },
    
    toString:function() {
    return "[Person " + this.name + "]";
    }
    };
    
    var person1 = new Person("Nicholas");
    var person2 = new Person("Greg");
    console.log("sayHi" in person1); // false
    console.log("sayHi" in person2); // false
    
    // add a new method
    Person.prototype.sayHi= function() {
    console.log("Hi");
    };
    
    person1.sayHi(); // outputs "Hi"
    person2.sayHi(); // outputs "Hi"
    

    可以随时改变原型对象的能力在封印对象和冻结对象上有一个十分有趣的后果。当你在一个对象上使用Object.seal()或Object.freeze()时,完全是在操作对象的自有属性。你无法添加自有属性或改变冻结对象的自有属性,但仍然可以通过在原型对象上添加属性来扩展这些对象实例,如下例。

    var person1 = new Person("Nicholas");
    var person2 = new Person("Greg");
    Object.freeze(person1);
    Person.prototype.sayHi= function() {
    console.log("Hi");
    };
    person1.sayHi(); // outputs "Hi"
    person2.sayHi(); // outputs "Hi"
    

    本例中有两个Person的对象实例。person1是冻结对象而person2是普通对象。当你在原型对象上添加sayHi()时,person1和person2都获得了这一新方法,这似乎不符合person1的冻结状态。其实,[[Prototype]]属性是对象实例的自有属性,属性本身被冻结,但其指向的值(原型对象)并没有冻结。

    继承

    原型对象链和Object.prototype

    继承自Object.prototype的方法

    方法 说明
    hasOwnProperty() 检查是否存在一个给定名字的自有属性
    propertyIsEnumerable() 检查一个自有属性是否可枚举
    isPrototypeOf() 检查一个对象是否是另一个对象的原型对象
    valueOf() 返回一个对象的值表达
    toString() 返回一个对象的字符串表达

    valueOf()

    每当一个操作符被用于一个对象时就会调用valueOf()方法。valueOf()默认返回对象实例本身。

    var now = new Date();
    var earlier = new Date(2010, 1, 1);
    console.log(now >earlier); // true
    

    toString()

    一旦valueOf()返回的是一个引用而不是原始值的时候,就会回退调用toString()方法。

    var book = {
    title:"The Principles of Object-Oriented JavaScript"
    };
    var message = "Book = " + book;
    console.log(message); // "Book = [object Object]"
    
    var book = {
    title:"The Principles of Object-Oriented JavaScript",
    toString:function() {
    return "[Book " + this.title + "]"
    }
    };
    var message = "Book = " + book;
    // "Book = [Book The Principles of Object-Oriented JavaScript]"
    console.log(message);
    

    修改Object.prototype

    所有的对象都默认继承自Object.prototype,所以改变Object.prototype会影响所有的对象,这是非常危险的。

    Object.prototype.add= function(value) {
    return this + value;
    };
    var book = {
    title:"The Principles of Object-Oriented JavaScript"
    };
    console.log(book.add(5)); // "[object Object]5"
    console.log("title".add("end")); // "titleend"
    // in a web browser
    console.log(document.add(true)); // "[object HTMLDocument]true"
    console.log(window.add(5)); // "[object Window]true"
    

    对象继承

    var person1 = {
    name:"Nicholas",
    sayName:function() {
    console.log(this.name);
    }
    };
    var person2 = Object.create(person1, {
        name:{
        configurable:true,
        enumerable:true,
        value:"Greg",
        writable:true
        }
    });
    person1.sayName(); // outputs "Nicholas"
    person2.sayName(); // outputs "Greg"
    console.log(person1.hasOwnProperty("sayName")); // true
    console.log(person1.isPrototypeOf(person2)); // true
    
    console.log(person2.hasOwnProperty("sayName")); // false
    
    image.png

    构造函数继承

    构造函数的所有对象实例共享同一个原型对象,所以它们都继承自该对象,但不能用原型对象继承自有属性。

    function Rectangle(length, width) {
      this.length = length;
      this.width = width;
    }
    Rectangle.prototype.getArea = function () {
      return this.length * this.width;
    };
    Rectangle.prototype.toString = function () {
      return "[Rectangle " + this.length + "x" + this.width + "]";
    };
    // inherits from Rectangle
    function Square(size) {
      this.length = size;
      this.width = size;
    }
    Square.prototype = new Rectangle();
    Square.prototype.constructor = Square;
    Square.prototype.toString = function () {
      return "[Square " + this.length + "x" + this.width + "]";
    };
    var rect = new Rectangle(5, 10);
    var square = new Square(6);
    
    console.log(rect.getArea()); // 50
    console.log(square.getArea()); // 36
    
    console.log(rect.hasOwnProperty("getArea")); // true
    console.log(square.hasOwnProperty("getArea")); // false
    console.log(square.isPrototypeOf(rect)); // true
    
    console.log(rect.toString()); // "[Rectangle 5x10]"
    console.log(square.toString()); // "[Square 6x6]"
    console.log(rect instanceof Rectangle); // true
    console.log(rect instanceof Object); // true
    console.log(square instanceof Square); // true
    console.log(square instanceof Rectangle); // true
    console.log(square instanceof Object); // true
    
    image.png

    总结

    JavaScript通过原型对象链支持继承。当将一个对象的[[Prototype]]设置为另一个对象时,就在这两个对象之间创建了一条原型对象链。所有的泛用对象都自动继承自Object.prototype。如果你想要创建一个继承自其他对象的对象,你可以用Object.create()指定[[Prototype]]为一个新对象。

    自有属性/方法通过构造函数定义,共有属性/方法通过原型对象继承!!

    对象模式

    虽然JavaScript没有一个正式的私有(局部)属性的概念(ES6中出现了let语法,可以定义局部变量),但是可以创建仅在对象内可以访问的数据或函数。使用模块模式可对外界隐藏数据;也可以使用立即调用函数表达(IIFE)定义仅可被新创建的对象访问的本地变量和函数。

    模块暴露

    var person = (function() {
    var age = 25;
    function getAge() {
    return age;
    }
    function growOlder() {
    age++;
    }
    return{
    name:"Nicholas",
    getAge:getAge,
    growOlder:growOlder
    };
    }());
    

    私有成员和特权成员

    构造函数的私有成员

    function Person(name) {
      // define a variable only accessible inside of the Person constructor
    // 私有变量
      var age = 25;
      this.name = name;
      this.getAge = function () {
        return age;
      };
       this.setAge = function (data) {
         age = data;
       };
      this.growOlder = function () {
        age++;
      };
    }
    var person = new Person("Nicholas");
    console.log(person.name); // "Nicholas"
    console.log(person.getAge()); // 25
    person.age = 100;
    console.log(person.getAge()); // 25
    person.growOlder();
    console.log(person.getAge()); // 26
    
    var person = new Person("Nicholas");
    console.log(person.name); // "Nicholas"
    console.log(person.getAge()); // 25
    person.setAge(100);
    console.log(person.getAge()); // 100
    person.growOlder();
    console.log(person.getAge()); // 101
    
    

    与上例对比结果,没有使用this的属性是私有属性,同时将方法直接放在对象实例上不如放在其原型对象里有效。

    function Person(name) {
        this.age = 25;
        this.name = name;
    }
    Person.prototype.getAge = function () {
        return this.age;
    };
    
    Person.prototype.growOlder = function () {
        return this.age++;
    };
    
    Person.prototype.setAge = function (data) {
        this.age = data;
    };
    
    var person = new Person("Nicholas");
    console.log(person.name); // "Nicholas"
    console.log(person.getAge()); // 25
    person.age = 100;
    console.log(person.getAge()); // 100
    person.growOlder();
    console.log(person.getAge()); // 101
    
    var p2 = new Person("Nicholas");
    console.log(p2.name); // "Nicholas"
    console.log(p2.getAge()); // 25
    p2.age = 10;
    console.log(p2.getAge()); // 10
    p2.growOlder();
    console.log(p2.getAge()); // 11
    console.log(person.getAge()); // 101
    

    作用域安全的构造函数

    构造函数也是函数,所以可以不用new操作符直接调用它们来改变this的值。在非严格模式下,this被强制指向全局对象,这么做会导致无法预知的结果。而在严格模式下,构造函数会抛出一个错误。

    function Person(name) {
      this.name = name;
    }
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
    
    //由于Person构造函数不是用new操作符调用的,我们创建了一个全局变量name。
    var person1 = Person("Nicholas"); // note:missing "new"
    console.log(person1 instanceof Person); // false
    console.log(typeof person1); // "undefined"
    console.log(name); // "Nicholas"
    
    function Person(name){
        this.name = name;
    }
    var p1 = new Person("ligang");
    var p2 = Person("camile");
    console.log(p1 instanceof Person); // true
    console.log(p2 instanceof Person); // false
    

    作用域安全的构造函数是用不用new都可以被调用来生成新的对象实例的构造函数。其this在构造函数一开始执行就已经指向自定义类型的实例。当然,你可以根据new的使用与否决定构造函数的行为。

    function Person(name){
        if(this instanceof Person){
            this.name = name;
        }else {
            return new Person(name);
        }
    
    }
    var p = Person("ligang");
    console.log(p instanceof Person); // true
    

    许多内建构造函数,例如Array、RegExp不需要new也可以工作,正是因为它们被设计之初采用了作用域安全的构造函数。

    参考部分

    前端的边界

    https://www.cnblogs.com/czaiz/p/5748553.html

    https://blog.csdn.net/ligang2585116/article/details/54236109

    https://blog.csdn.net/ligang2585116/article/details/53522741

    前端的边界

    了解 CSS

    相关文章

      网友评论

          本文标题:Javascript 面向对象精要

          本文链接:https://www.haomeiwen.com/subject/lmdwrktx.html