《JavaScript 面向对象精要》 阅读摘要(下)

简介: 高程面向对象这块内容介绍的比较浅显,个人觉得这本小书是高程的补充,看完之后觉得收获匪浅,所以做了个笔记,以备后询Js中两种基本数据类型:原始类型(基本数据类型)和引用类型;原始类型保存为简单数据值,引用类型则保存为对象,其本质是指向内存位置的应用。其它编程语言用栈存储原始类型,用堆存储引用类型,而js则不同:它使用一个变量对象追踪变量的生存期。原始值被直接保存在变量对象里,而引用值则作为一个指针保存在变量对象内,该指针指向实际对象在内存中的存储位置。

5. 继承


5.1 原型对象链和Object.prototype


Js内建的继承方法被称为原型对象链,又称为原型对象继承。原型对象的属性可以由对象实例访问。实例对象集成了原型对象的属性,因为原型对象也是一个对象,它也有自己的原型对象并继承其属性。这就是原型继承链:对象继承其原型对象,而原型对象继承它的原型对象,以此类推。所有的对象,包括自定义的对象都继承自Object,除非另有指定。更确切的说,所有对象都继承自Object.prototype,任何以字面量形式定义的对象,其[[Prototype]]的值都被设为Object.prototype,这意味着它继承Object.prototype的属性。


var book = {title: 'a book'}
console.log(Object.getPrototypeOf(book) === Object.prototype)            // true
复制代码


5.1.1 继承自Object.prototype的方法


前几张用到的几个方法都是定义在Object.prototype上的,因此可以被其他对象继承:


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


这几种方法由继承出现在所有的对象中,当你需要对象在Js中以一致的方式工作时,最后两个尤为重要。


  1. valueOf()每当一个操作符被用于一个对象时就会调用valueOf()方法,其默认返回对象实例本身。原始封装类型重写了valueOf()以使得它对String返回一个字符串,对Boolean返回一个布尔,对Number返回一个数字;类似的,对Date对象的valueOf()返回一个epoch时间,单位是毫秒(正如Data.prototype.getTime())。


var now = new Date                // now.valueOf()  === 1505108676169
var earlier = new Date(2010,1,1)            // earlier.valueOf() === 1264953600000
console.log(now>earlier)                // true
console.log(now-earlier)                 // 240155076169
复制代码


  1. now是一个代表当前时间的Date,而earlier是过去的时间,当使用操作符>时,两个对象上都调用了valueOf()方法,你甚至可以用两个Date相减来获得它们在epoch时间上的差值。如果你的对象也要这样使用操作符,你可以定义自己的valueOf()方法,定义的时候你并没有改变操作符的行为,仅仅应了操作符默认行为所使用的值。
  2. toString()一旦valueOf()返回的是一个引用而不是原始值的时候,就会回退调用toString()方法。另外,当Js期望一个字符串时也会对原始值隐式调用toString()。例如当加号操作符的一边是一个字符串时,另一边就会被自动转换成字符串,如果另一边是一个原始值,会自动转换成一个字符串表达(true => "true"),如果另一边是一个引用值,则会调用valueOf(),如果其返回一个引用值,则调用toString()


var book = {title: 'a book'}
console.log("book = " + book)                // "book = [object Object]"
复制代码


  1. 因为book是一个对象,因此调用它的toString()方法,该方法继承自Object.prototype,大部分Js引擎返回默认值[object Object],如果对这个值不满意可以复写,为此类字符串提供包含跟多信息。


var book = {title: 'a book',
toString(){return `[Book = ${this.title} ]`}}
console.log("book = " + book)                 // book = [Book = a book ]
复制代码


5.1.2 修改Object.prototype


所有的对象都默认继承自Object.prototype,所以改变它会影响到所有的对象,这是非常危险的。 如果给Obejct.prototype添加一个方法,它是可枚举的,可以粗现在for-in循环中,一个空对象依然会输出一个之前添加的属性。尽量不要修改Object.prototype。


5.2 对象继承


对象字面量形式会隐式指定Object.prototype为其[[Prototype]],也可以用Object.create()方式显示指定。Object.create()方法接受两个参数:需要被设置为新对象[[Prototype]]的对象、属性描述对象,格式如在Object.defineProperties()中使用的一样(第三章)。


var book = {title: 'a book'}
// ↑ 等价于 ↓
var book = Object.create(Object.prototype, {
    title: {
        configurable: true,
        enumerable: true,
        value: 'a book',
        writable: true
    }
})
复制代码


第一种写法中字面量形式定义的对象自动继承Object.prototype且其属性默认设置为可配置、可写、可枚举。第二种写法显示使用Object.create()做了相同的操作,两个book对象的行为完全一致。


var person = {
    name: "Jack",
    sayName: function(){
        console.log(this.name);
    }
}
var student = Object.create(person, {
    name:{value: "Ljc"},
    grade: {
        value: "fourth year of university",
        enumerable: true,
        configurable: true,
        writable: true
    }
});
person.sayName(); // "Jack"
student.sayName(); // "Ljc"
console.log(person.hasOwnProperty("sayName")); // true
console.log(person.isPrototypeOf(student)); // true
console.log(student.hasOwnProperty("sayName")); // false
console.log("sayName" in student); // true
console.log(student.__proto__===person)                                      // true
console.log(student.__proto__.__proto__===Object.prototype)      // true
复制代码


微信截图_20220427162822.png


对象person2继承自person1,也就集成了person1的name和sayName(),然而又通过重写name属性定义了一个自有属性,隐藏并替代了原型对象中的同名属性。所以person1.sayName()输出Nicholas而person2.sayName()输出Greg。 在访问一个对象的时候,Js引擎会执行一个搜索过程,如果在对象实例上发现该属性,该属性值就会被使用,如果没有发现则搜索[[Prototype]],如果仍然没有发现,则继续搜索该原型对象的[[Prototype]],知道继承链末端,末端通常是一个Object.prototype,其[[prototype]]为null。这就是原型链。 当然也可以通过Object.create()创建[[Prototype]]为null的对象:var obj=Object.create(null)。该对象obj是一个没有原型链的对象,这意味着toString()valueOf等存在于Object原型上的方法都不存在于该对象上。


5.3 构造函数继承


Js中的对象继承也是构造函数继承的基础,第四章提到:几乎所有的函数都有prototype属性(通过Function.prototype.bind方法构造出来的函数是个例外),它可以被替换和修改。该prototype属性被自动设置为一个继承自Object.prototype的泛用对象,该对象有个自有属性constructor


// 构造函数
function YourConstructor() {}
// Js引擎在背后做的:
YourConstructor.prototype = Object.create(Object.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: YourConstructor,
        writable: true
    }
})
console.log(YourConstructor.prototype.__proto__===Object.prototype)            // true
复制代码


你不需要做额外工作,Js引擎帮你把构造函数的prototype属性设置为一个继承自Object.prototype的对象,这意味着YourConstructor创建出来的任何对象都继承自Object.prototype,YouConstructor是Object的子类。 由于prototype可写,可以通过改写它来改变原型链:


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} ]`};
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 squa = new Square(6)
console.log(rect instanceof Rectangle)        // true
console.log(rect instanceof Square)        // false
console.log(rect instanceof Object)        // true
console.log(squa instanceof Rectangle)        // true
console.log(squa instanceof Square)        // true
console.log(squa instanceof Object)        // true
复制代码


MDN:instanceof 运算符可以用来判断某个构造函数的 prototype 属性是否存在另外一个要检测对象的原型链上。


Square构造函数的prototype属性被改写为Rectagle的一个实例,此时不需要给Rectangle的调用提供参数,因为它们不需要被使用,而且如果提供了,那么所有的Square对象实例都会共享这样的维度。如果用这种方式改写原型链,需要确保构造函数不会再参数缺失时抛出错误(很多构造函数包含的初始化逻辑)且构造函数不会改变任何全局状态。


// inherits from Rectangle
function Square(size){
    this.length = size;
    this.width = size;
}
Square.prototype = new Rectangle(); // 尽管是 Square.prototype 是指向了 Rectangle 的对象实例,即Square的实例对象也能访问该实例的属性(如果你提前声明了该对象,且给该对象新增属性)。
// Square.prototype = Rectangle.prototype; // 这种实现没有上面这种好,因为Square.prototype 指向了 Rectangle.prototype,导致修改Square.prototype时,实际就是修改Rectangle.prototype。
console.log(Square.prototype.constructor); // 输出 Rectangle 构造函数
Square.prototype.constructor = Square; // 重置回 Square 构造函数
console.log(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.toString()); // "[Rectangle 5 * 10]", 但如果是Square.prototype = Rectangle.prototype,则这里会"[Square 5 * 10]"
console.log(square.toString()); // "[Square 6 * 6]"
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
复制代码


微信截图_20220427162804.png


Square.prototype 并不真的需要被改成为一个 Rectangle对象。事实上,是 Square.prototype 需要指向 Rectangle.prototype 使得继承得以实现。这意味着可以用 Object.create() 简化例子。


// inherits from Rectangle
function Square(size){
    this.length = size;
    this.width = size;
}
Square.prototype= Object.create(Rectangle.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: Square,
        writable: true
    }
})
复制代码


在对原型对象添加属性前要确保你已经改写了原型对象,否则在改写时会丢失之前添加的方法(因为继承是将被继承对象赋值给需要继承的原型对象,相当于重写了需要继承的原型对象)。


5.4 构造函数窃取


由于JavaScript中的继承是通过原型对象链来实现的,因此不需要调用对象的父类的构造函数。如果确实需要在子类构造函数中调用父类构造函数,那就可以在子类的构造函数中利用 call、apply方法调用父类的构造函数。


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} ]`};
function Square(size) {Rectangle.call(this, size, size)}
Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        value: Square,
        enumerable: true,
        configurable: true,
        writable: true
    }
})
Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`}
var rect = new Rectangle(5, 10)
var squa = new Square(6)
console.log(rect.getArea())
console.log(rect.toString())
console.log(squa.getArea())
console.log(squa.toString())
复制代码


一般来说,需要修改 prototype 来继承方法并用构造函数窃取来设置属性,由于这种做法模仿了那些基于类的语言的类继承,所以这通常被称为伪类继承


5.5 访问父类方法


其实也是通过指定 callapply 的子对象调用父类方法。


6. 对象模式


可以使用继承或者混入等其他技术令对象间行为共享,也可以利用Js高级技巧阻止对象结构被改变。


6.1 私有成员和特权成员


6.1.1 模块模式


模块模式是一种用于创建拥有私有数据的单件对象的模式。 基本做法是使用立即调用函数表达式(IIFE)来返回一个对象。原理是利用闭包。


var yourObj = (function(){
    // private data variables   
    return {
        // public methods and properties
    }
}());
复制代码


模块模式还有一个变种叫暴露模块模式,它将所有的变量和方法都放在 IIFE 的头部,然后将它们设置到需要被返回的对象上。


//  一般写法
var yourObj = (function(){
    var age = 25;    
    return {
        name: "Ljc",      
        getAge: function(){
            return age 
        }
    }
}());
// 暴露模块模式,保证所有变量和函数声明都在同一个地方
var yourObj = (function(){
    var age = 25;                            // 私有变量,外部无法访问
    function getAge(){
        return age
    };
    return {
        name: "Ljc",                          // 公共变量外部可以访问
        getAge: getAge                    // 外部可以访问的对象
    }
}());
复制代码


6.1.2 构造函数的私有成员


模块模式在定义单个对象的私有属性十分有效,但对于那些同样需要私有属性的自定义类型呢?你可以在构造函数中使用类似的模式来创建每个实例的私有数据。


function Person(name){
    // define a variable only accessible inside of the Person constructor
    var age = 22;   
    this.name = name;
    this.getAge = function(){return age;};
    this.growOlder = function(){age++;}
}
var person = new Person("Ljc");
console.log(person.age);         // undefined
person.age = 100;
console.log(person.getAge());         // 22
person.growOlder();
console.log(person.getAge());         // 23
复制代码


构造函数在被new的时候创建了一个本地作用于并返回this对象。这里有个问题:如果你需要对象实例拥有私有数据,就不能将相应方法放在 prototype上。 如果你需要所有实例共享私有数据(就好像它被定义在原型对象里那样),则可结合模块模式和构造函数,如下:


var Person = (function(){
    var age = 22;
    function InnerPerson(name){this.name = name;}
    InnerPerson.prototype.getAge = function(){return age;}
    InnerPerson.prototype.growOlder = function(){age++;};
    return InnerPerson;
}());
var person1 = new Person("Nicholash");
var person2 = new Person("Greg");
console.log(person1.name); // "Nicholash"
console.log(person1.getAge()); // 22
console.log(person2.name); // "Greg"
console.log(person2.getAge()); // 22
person1.growOlder();
console.log(person1.getAge()); // 23
console.log(person2.getAge()); // 23
复制代码


6.2 混入


这是一种伪继承。一个对象在不改变原型对象链的情况下得到了另外一个对象的属性被称为“混入”。因此,和继承不同,混入让你在创建对象后无法检查属性来源。


function mixin(receiver, supplier){
    for(var property in supplier){
        if(supplier.hasOwnProperty(property)){
            receiver[property] = supplier[property];
        }
    }
}
复制代码


这是浅拷贝,如果属性的值是一个引用,那么两者将指向同一个对象。 要注意一件事,使用这种方式,supplier的访问器属性会被复制为receiver的数据属性。


function mixin(reciver, supplier) {
    if (Object.getOwnPropertyDescriptor) {                    // 检查是否支持es5
        Object.keys(supplier).forEach(property => {
            var descriptor = Object.getOwnPropertyDescriptor(supplier, property)
            Object.defineProperty(reciver, property, descriptor)
        })
    } else {
        for (var property in supplier) {                        // 否则使用浅复制
            if (supplier.hasOwnProperty(property)) {
                reciver[property] = supplier[property]
            }
        }
    }
}
复制代码


6.3 作用域安全的构造函数


构造函数也是函数,所以不用 new 也能调用它们来改变 this 的值。在非严格模式下, this 被强制指向全局对象。而在严格模式下,构造函数会抛出一个错误(因为严格模式下没有为全局对象设置 this,this 保持为 undefined)。 而很多内建构造函数,例如 Array、RegExp 不需要 new 也能正常工作,这是因为它们被设计为作用域安全的构造函数。 当用 new 调用一个函数时,this 指向的新创建的对象已经属于该构造函数所代表的自定义类型。因此,可在函数内用 instanceof 检查自己是否被 new 调用。


function Person(name){
    if(this instanceof Person){
        // called with "new"
    }else{
        // called without "new"
    }
}
复制代码


具体案例:


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



相关文章
|
5月前
js- 面向对象进阶
Object.defineProperty等面向对象的信息
|
5月前
|
存储 JavaScript 前端开发
|
23天前
|
设计模式 前端开发 JavaScript
深入认识:JavaScript中的面向对象
深入认识:JavaScript中的面向对象
5 0
|
30天前
|
Web App开发 JavaScript 前端开发
深度刨析 JavaScript 模拟面向对象的内部机制
深度刨析 JavaScript 模拟面向对象的内部机制
77 0
|
1月前
|
存储 JavaScript 前端开发
【JavaScript】<面向对象Object>函数方法&对象创建&原型对象&作用域解析
【1月更文挑战第17天】【JavaScript】<面向对象Object>函数方法&对象创建&原型对象&作用域解析
|
1月前
|
存储 JSON 前端开发
JavaScript:构造函数&面向对象
JavaScript:构造函数&面向对象
33 2
|
1月前
|
存储 JavaScript 前端开发
JavaScript知识总结 终结篇--面向对象,垃圾回收与内存泄漏
JavaScript知识总结 终结篇--面向对象,垃圾回收与内存泄漏
|
6月前
|
设计模式 JavaScript 前端开发
js面向对象入门
js面向对象入门
51 0
|
5月前
|
存储 JavaScript
js-面向对象
把数据及对数据的操作方法放在一起,作为一个相互依存的整体——对象。对同类对象抽象出其共性,形成类。类中的大多数数据,只能用本类的方法进行处理。
|
5月前
|
JSON JavaScript 前端开发

相关产品

  • 云迁移中心