对象、类与面向对象编程

《JavaScript高级程序设计》读书笔记

理解对象

属性的类型

对象属性分为:数据属性和访问器属性

数据属性:

  • configurable:表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true
  • enumberable:表示属性是否可以通过for-in 循环返回。默认为true
  • writable:表示属性的值是否可以被修改。默认为true
  • value:包含属性实际的值
let person = {};
Object.defineProperty(person, "name", {
    writable:false,
    value: "Nicholas"
});
console.log(person.name); 'Nicholas'

访问器属性:

  • configurable:表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认为true
  • enumerable:表示属性是否可以通过for-in 循环返回。默认为true
  • get:获取函数,在读取属性时调用
  • set:设置函数,在写入属性时调用

访问器属性不能直接定义,必须使用 Object.defineProperty(obj, prop, descriptor)

let book = {
    year_: 2017,
    edition: 1
};
Object.defineProperty(book, "year",{
    get(){
        return this.year_;
    },
    set(newValue){
        if(newValue > 2017){
            this.year_ = newValue;
            this.edition ++;
        }
    }
});
book.year = 2018;
book.edition; // 2

Object.defineProperty(obj, prop, descriptor) :修改属性默认特性

Object.defineProperties(obj, props) :一次定义多个属性默认特性

Object.getOwnPropertyDescriptor(obj, prop):返回指定对象上一个自有属性对应的属性描述

Object.getOwnPropertyDescriptors(obj):返回指定对象的所有自身属性的描述符

Object.assign(target, ...sources):将所有可枚举(Object.propertyIsEnumerable() 返回 true)和自有(Object.hasOwnProperty() 返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。浅复制

Object.is(value1, value2):判断两个值是否为同一个值。考虑到了一些边界处理。如true 和 1

增强的对象语法

1. 属性值简写

let name  = 'Matt';
let person = {
    name: name
};
// 简写
let person = {
    name
}

2. 可计算属性

中括号包围的对象属性将作为Js 表达式而不是字符串求值。

const nameKey = 'name';
let person = {};
person[nameKey] = 'Matt';
// 有了计算属性
let person = {
    [nameKey]: 'Matt'
}

3. 简写方法名

let person = {
    sayName: function(name){
        console.log('kkk');
    }
};
//简写
let person = {
    sayName(){
        console.log('kkk');
    }
}

对象解构

  • 解构在内部使用函数ToObject() 把源数据结构转化为对象,这意味着原始值会被当成对象。
  • 如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中。
  • 在外层属性没有定义的情况下不能使用嵌套解构。
  • 如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值失败,则整个解构赋值只会完成一部分。

创建对象

工厂模式

用于抽象创建特定对象的过程。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题

function createPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    }
    return o;
}
let person1 = createPerson("Nicholas", 29, "worker")

构造函数模式

创建特定类型对象。

与工厂模式区别:

  • 没有显示地创建对象
  • 属性和方法直接赋值给了this
  • 没有return

要创建这样的实例,应该使用new 操作符。以这种方法调用构造函数会执行如下操作:

  • 在内存中创建一个新对象
  • 这个新对象内部 [[Prototype]] 特性被赋值为构造函数的prototype 属性
  • 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)
  • 执行构造函数内部的代码(给新对象添加属性)
  • 如果构造函数返回非空对象,即返回该对象;否则,返回刚刚创建的新对象。
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
};
let person1 = new Person('Bob', 18, 'worker');
let person2 = new Person('Alice', 23, 'writer');
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true

1. 构造函数也是函数

构造函数与普通函数唯一区别就是调用方法不同。

// 作为构造函数
let person1 = new Person('Bob', 18, 'worker');

// 作为函数调用(在调用一个函数没有明确设置this值的情况下,this始终指向Global 对象,所以window出现了sayName() 方法)
Person('Greg', 27, "Doctor");
window.sayName(); // Greg

// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //Kristen

2. 构造函数的问题

其定义的方法都会在每个实例上都创建一遍。即person1 和 person2 都有名为sayName() 的方法,但这两个方法不是同一个Function 实例。因为都是做一样的事,所以没必要定义两个不同的Function 实例,要解决这个问题,可以把函数定义到构造函数外部,但造成了全局污染,这个问题可以通过原型模式来解决。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)"); // 逻辑等价
};

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。

function Person() {}
Person.prototype.name = 'Bob';
Person.prototype.age = 29;
Person.prototype.sayName = function() {
    console.log(this.name);
};
let person1 = new Person();
person1.sayName(); //Bob

这里所有属性和sayName() 方法都直接添加到了Person 的prorotype 属性上,构造函数体中什么也没有。与构造函数模式不同,使用这种原型模式定义后的属性和方法是由所有实例共享的。

1. 理解原型

无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor 的属性指向与之关联的构造函数。(两者循环引用)

在自定义构造函数时,原型对象默认只会获得constructor 属性,其他的所有方法都继承自Object。每次调用构造函数创建一个新的实例,这个实例内部[[Prototype]] 指针就会被赋值为构造函数的原型对象,这个特性会在每个对象上暴露__proto__ 属性,通过这个属性可以访问对象的原型。实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。

function Person() {}

// 声明之后,构造函数就有了一个与之关联的原型对象
console.log(typeof Person.prototype); // Object
console.log(Person.prototype);
//{constructor: ƒ}
// constructor: ƒ Person()
// [[Prototype]]: Object

// 构造函数有一个prototype 属性引用其原型对象,而这个原型对象有个constructor 属性,
// 引用这个构造函数。两者循环引用
console.log(Person.prototype.constructor === Person); // true

// 正常的原型链都会终止与Object 的原型对象,Object 原型的原型是null
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__); // null

let person1 = new Person(), person2 = new Person();

// 构造函数、原型对象、实例 是3个完全不同的对象
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true

// 实例通过__proto__ 链接到原型对象,它实际上指向藏特性[[Prototype]]
// 构造函数通过prototype 属性链接到原型对象
// 实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.constructor === Person); // true

// 同一构造函数创建的两个实例,共享同一个原型对象
console.log(person1.__proto__ === person2.__proto__); // true

// instanceof 检查实例的原型链中是否包含指定的构造函数的原型
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
  • prototypeObj.isPrototypeOf(object) 方法用于测试一个对象是否存在于另一个对象的原型链上。
  • Object.getPrototypeOf(object) 方法返回指定对象的原型(内部[[Prototype]]属性的值)。
  • Object.setPrototypeOf(obj, prototype) 方法设置一个指定的对象的原型 ( 即,内部 [[Prototype]] 属性)到另一个对象或  null。(严重影响代码性能,推荐Object.create())
  • Object.create(proto,[propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__。

2. 原型层级

在通过对象访问属性时,会从对象实例开始,要是没有找到继续沿着指针进入原型对象,如果找到则返回。

obj.hasOwnProperty(prop) 方法会返回一个布尔值,用于确定某个属性时在实例上还是在原型对象上。

3. 原型和in 操作符

两种方法使用:单独使用和在for-in 循环中使用。

单独使用时,in 操作符会在可以通过对象访问指定属性返回true,无论该属性是在实例上还是在原型上。

在for-in 循环中使用时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。要获得对象上所有可枚举的实例属性,可以使用Object.keys() 方法。如果想列出所有实例属性,无论是否可以枚举,可以使用 Object.getOwnPropertyNames() ,兄弟方法 Object.getOwnPropertySymbols() 方法,只针对符号。

4. 属性枚举顺序

for-in循环 和Object.keys() 的枚举顺序不确定,取决于js 引擎

Object.getOwnPropertyNames() 、Object.getOwnPropertySymbols() 和 Object.assign() 的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面中定义的键以它们逗号分隔的顺序插入。

对象迭代

  • Object.values() 接收一个对象,返回对象值数组
  • Object.entries() 接收一个对象,返回键/值对的数组

继承

实现继承:继承实际的方法

原型链

通过原型继承多个引用类型的属性和方法。每个构造函数都有一个原型对象,原型有一个属性指向构造函数,而实例有一个内部指针指向原型。原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数,这样就在实例与原型之间构造了一条原型链。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}
// 继承
SubType.prototype = new SuperType();
let instance = new SubType();
console.log(instance.getSuperValue());

1. 默认原型

默认情况下,所有引用类型都继承自Object ,这也是通过原型链实现的。任何函数的默认原型都是一个Object 的实例,这意味着这个实例有一个内部指针指向 Object.prototyep。

2. 原型与继承关系

  • instanceof 如果一个实例的原型链中出现相应的构造函数,则返回true
  • isProtoTypeOf 只要原型链中包含这个原型,则返回true

3. 关于方法

子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。

以对象字面量方法创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

4. 原型链的问题

原型中包含的引用值会在所有实例间共享。

子类型在实例化时不能给父类型的构造函数传参。

盗用构造函数

为了解决原型包含引用值导致的继承问题。基本思路:在子类构造函数中调用父类构造函数。使用apple() 和 call() 方法以新创建的对象为上下文执行构造函数。

function SuperType(name) {
    this.colors = ['red', 'blue'];
    this.name = name;
}
function SubType() {
    SuperType.call(this, "Bob");
}
let instance = new SubType();
instance.colors.push('balck');
console.log(instance.colors);

1. 传递参数

可以在子类构造函数中向父类构造函数传参

2. 盗用构造函数的问题

必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式

组合继承

也叫伪经典继承,基本思路:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function SuperType(name) {
    this.colors = ['red', 'blue'];
    this.name = name;
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}
function SubType(name ,age){
    // 继承属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
    console.log(this.age);
}
let instance = new SubType("Bob", 20);
instance.colors.push('black');
console.log(instance.colors); // ['red', 'blue', 'black']
instance.sayName(); // Bob

原型式继承

你有一个对象,想在它基础上在创建一个新对象。你需要把这个对象先传给object() ,然后再对返回的对象进行适当修改。本质上,object() 是对传入的对象执行了一次浅复制。

function object(o){
    function F() {}
    F.prototype = o;
    return new F();
}

ECMAScript 5 通过增加Object.create() 方法将原型式继承的概念规范化。 

寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original) {
    let clone = object(original); // 通过调用函数创建一个新对象
    clone.sayHi = functin() { // 以某种方式增强这个对象
        console.log('hi');
    };    
    return clone; // 返回这个对象
}

寄生式组合继承

不通过调用父类构造函数给予子类原型赋值,而是取得父类原型的一个副本。

function inheritPrototype(subType, superType) {
    let prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType;    // 增强对象
    subtype.prototype = prototype;    // 赋值对象
}

类定义

class Person{} 、const Animal = class {}

类没有声明提升,受块作用域限制。

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态方法。

类构造函数

constructor关键字用于在类定义块内部创建类的构造函数。方法名 constructor会告诉解释器在使用new 操作符创建类的新实例时,应该调用这个函数。

1. 实例化

使用new调用类的构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]] 指向被赋值为构造函数的prototype 属性
  3. 构造函数内部的this 被赋值为这个新对象
  4. 执行构造函数内部的代码
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的对象

类实例化时传入的参数会用作构造函数的参数。默认情况下,类构造函数会在执行之后返回this对象。

类构造函数被实例化之后,会成为普通的实例方法。

2. 把类当成特殊函数

实例、原型和类成员

1. 实例成员

每次使用new,都会执行类构造函数(constructor),为这个实例添加自有属性。每个实例都对应一个唯一的成员对象,所有成员不会在原型上共享。

2. 原型方法与访问器

为了在实例间共享方法,会把在类块中定义的方法作为原型方法。类方法等同于对象属性,因此可以使用字符串、符号和计算的值作为键。

class Person {
    constructor() {
        // 添加到this的所有内容都会存在于不同的实例上
        this.localte = ()=>console.log('localte');
    }
    // 类块中定义的所有内容都会定义在类的原型上
    localte(){
        console.log('prototype');
    }
    // 可以使用计算的值
    ['computed' + 'Key'](){
        console.log('computedKey');
    }
}

类定义也支持获取和设置访问器。

class Person{
    set name(newName){
        this.name_ = newName;
    }
    get name(){
        return this.name_;
    }
}

3. 静态类方法

这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,每个类上只能有一个静态成员。使用static 关键字为前缀,内部的this 引用类自身。

class Person {
    constructor(age) {
        this.age_ = age;
    }
    sayAge() {
        console.log(this.age_);
    }
    static create(){
        return new Person(Math.floor(Math.random()*100));
    }
}

Person.create();

4. 非函数原型和类成员

类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加。

class Person {}
// 在类上定义数据成员
Person.greetring = 'My name is Bob';
// 在原型上定义数据成员
Person.prototype.name = "Bob";

5. 迭代器与生成器方法

class Person {
    constructor(){
        this.nickname = ['Jack', 'Bob'];
    }
    // 在原型上定义生成器方法
    *createNickname() {
        yield 'Jack';
        yield 'Bob';
    }
    // 在类上定义生成器方法
    static *createname() {
        yield 'Jack';
        yield 'Bob';
    }
    // 添加默认的迭代器
    *[Symbol.iterator]() {
        yield *this.nickname.entries();
    }
}

继承

1. 继承继承

使用extends 关键字,可以继承任何拥有[[Construct]] 和原型的对象。

class Vehicle {}
// 继承类
class Bus extends Vehicle{}

function Person() {}
// 继承普通构造函数
class D extends Person{}

2. 构造函数、HomeObject 和 super()

ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。super 始终会定义为 [[HomeObject]] 的原型。

派生类的方法可以通过super 关键字引用它们的原型。只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。

class Vehicle {
    constructor() {
        this.hasEngine = true;
    }
    static identify(){
        console.log('identify');
    }
}
class Bus extends Vehicle {
    constructor() {
        // 不要在super 之前引用this
        super(); // 相当于super.constructor()
        console.log(this);
    }
    static identify() {
        super.identify();
    }
}

使用super 注意的问题:

  • super 只能在派生类构造函数和静态方法中使用
  • 不能单独引用super 关键字,要么用它调用构造函数,要么用它引用静态方法
  • 调用 super() 会调用父类构造函数,并将返回的实例赋值给this
  • super() 的行为如同调用构造函数,如果需要给父类构造函数传参,需要手动传入
  • 如果没有定义类构造函数,在实例化派生类时会调用super() ,而且会传入所有传给派生类的参数
  • 在类构造函数中,不能再调用super() 之前引用this
  • 如果派生类中显式定义了构造函数(constructor),则要么必须在其中调用super(),要么必须在其中返回一个对象

3. 抽象基类

有时候需要定义这样一个类,它可供其他类继承,但本身不会被实例化。new.target 在实例化时检测是不是抽象基类

class Vehicle {
    constructor() {
        console.log(new.target);
        if(new.target === Vehicle) {
            throw new Error("Vehicle 不能被实例化");
        }
        // 要求派生类必须定义某个方法
        if(!this.foo){
             throw new Error("未定义 foo()");
        }
    }
}

4. 继承内置类型

class SuperArray extends Array {
    shuffile() {
        console.log('扩展内置类型');
    }
    static get [Symbol.species]() {
        console.log('覆盖默认行为')
        return Array;
    }
}

5. 类混入

class Vehicle{}
let A = (Superclass) => class extends Superclass{
    foo() {
        console.log('foo-a');
    }
}
let B = (Superclass) => class extends Superclass{
    bar() {
        console.log('bar-b');
    }
}
let C = (Superclass) => class extends Superclass{
    baz() {
        console.log('baz-c');
    }
}

class Bus extends A(B(C(Vehicle))) {}

// 或者写一个辅助函数,可以把嵌套展开
function mix(BaseClass, ...Mixins){
    return Mixins.reduce((item, current) => current(item), BaseClass);
}
class Bus extends mix(Vehicle, A, B, C){}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞天巨兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值