对象与原型链。

75 阅读10分钟

截屏2021-09-29 下午4.11.03.png

一、ES5中的类与继承

原型链继承

原型链是javascript的主要继承方式,主要思想是将父类的新实例复制给子类的原型。

function SuperType() {
    this.property = true;
}

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

function SubType() {
    this.subproperty = false;
}
//继承
SubType.prototype = new SuperType();
//将父类的实例赋值给子类的原型

实现继承的关键,在于subType没有使用默认原型,而是将其替换成了一个新的对象,该对象为superType的实例,不仅继承了属性和方法,还连接到了SuperType的prototype。

(1)判断实例与原型的关系

  • instanceof(实例的方法)

如果一个实例的原型链上出现过相应的构造函数,那么返回true,

  • isPrototypeof()(原型的方法)

只要原型链中包含这个原型,返回true

Object.isPrototypeOf(instance);

(2)原型链继承存在的问题

  1. 当原型中包含引用类型的变量时,一个实例对该属性修改会造成其他实例的该属性也会被修改
  2. 无法在不影响所有对象实例的情况下把参数传进父类的构造函数。

2. 盗用构造函数(对象伪装、经典继承)

通过在子类的构造函数中使用apply或者call方法调用父类的构造函数。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}

function SubType() {
    SuperType.call(this);
}

相当于复制父类的属性给子类,并没有用到原型。

优点:可以实现多继承,可以传递参数给父类型的构造函数。

缺点:必须在构造函数中定义方法,因此函数不能重用,子类也不能访问父类原型上定义的方法。

3.组合继承

综合了原型链和盗用构造函数**,基本思路是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性。**

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SubType(name, age) {
    //继承属性
		//第二次调用
    SuperType.call(this, name);
    this.age = age;
}
//继承方法
//第一次调用父元素的构造函数
SubType.prototype = new SuperType();

组合继承是JavaScript中使用最多的继承模式。

缺点:调用了两次父类构造函数,

4. 原型式继承

使用object.create()函数,接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)。

let person = {
	name:"Nicholas",
	friends: ["shelly", "sherlock"];
}
let anotherPerson = Object.create(person);

适用的情形:

  • 有一个对象,想在其基础上新建一个对象,传入的对象将作为新对象的原型,也就是同时传入了属性值和方法。
  • 非常适合不需要单独创建构造函数,但是仍然需要在对象间共享信息的场合。
  • 注意⚠️:属性中的引用值始终会在相关对象中共享!

5.寄生继承

思路和工厂模式类似,调用一个仅用于封装过程的函数。

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

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

以上方法接受一个参数,该参数为新对象的基准对象。

6.寄生式组合继承

主要是解决了组合继承的问题:调用了两次父类构造函数,使得相同的属性分别出现在了子类实例和子类原型上。

基本思路是不通过调用父类构造函数给子类原型赋值,而是使用寄生式继承来继承父类原型,将返回的新对象赋值给子类原型。使用盗用构造函数来继承父类实例的属性。

function inheritPrototype(subType, superType) {
    let prototype = Object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype =prototype;
}

接收两个参数:子类构造函数和父类构造函数

  1. 创建父类原型的一个副本,
  2. 给返回的prototype对象设置constructor属性,解决由于重写原型造成的constructor丢失问题。
  3. 将新创建的对象赋值给子类的原型。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}

function SubType(name, age) {
    //只调用了一次SuperType构造函数。避免了SubType.prototype上不必要也无用的属性。
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);

二、ES6中的类

class类是ES6中引入的新的关键字,实际上使用的仍旧是原型和构造函数的概念。

1.类的定义

与函数类似,有两种方式:

//类声明
class Person{}
//类表达式
const Person = class{}
  • 类定义不能提升。
  • 函数受函数作用域限制,类受块作用域限制

2.类的组成

构造函数、实例方法等,默认情况下,类中定义的代码都在严格模式下执行。

(1)类构造函数

使用constructor关键字定义构造函数。告诉解释器在使用new创建类的新实例时, 应该调用这个函数。

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

  • 在内存中创建一个新的对象
  • 这个对象内部的prototype指针(即__proto__,隐式原)指向构造函数的prototype
  • 构造函数内部的this被赋值给这个新对象
  • 执行构造函数内部的代码,给新对象添加属性
  • 如果构造函数返回非空对象那么返回(那么instanceof检测结果也可能和类无关),否则返回新创建的对象。

构造函数与类构造函数的区别:

  • 类构造函数必须使用new操作符进行调用。
  • 构造函数可以不使用new进行调用,就会以全局this(window)作为内部对象。

把类当作特殊函数

通过typeof进行检测,结果发现类就是function

类是JavaScript的一等公民,可以像其他对象或者函数引用一样把类作为参数传递,类可以像函数一样在任何地方定义。

类也可以向立即调用函数表达式一样,进行立即实例化。

let p = new class Foo{
	constructor(x){
		console.log(x);
	}
}('bar');

3.实例、原型与类成员

(1)实例成员

在构造函数内部添加的,可以为新创建的实例添加**自有属性,**不会在原型上共享。

class Person {
      constructor() {
//以下均为实例成员
				this.name = new String('Jack');
        this.sayName = () => console.log(this.name);
        this.nicknames = ['Jake', 'J-Dog']
      }
}

(2)原型方法与访问器

块中定义的方法作为原型方法,可以在实例之间进行共享。

class Person {
    constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
        this.locate = () => console.log('instance');
      }
		locate(){ console.log('prototype'); }
}

虽然可以通过在类块中定义方法将其作为原型方法,但是不可以在类块中直接给原型添加原始值或者对象作为成员数据。 虽然不显示支持在原型或者类上添加数据,但是可以在类外部定义,手动添加:

class Person{
	sayName(){
		console.log(`${Person.greeting} ${this.name}`);
	}
}
//在外部定义:

//1. 在类上定义成员:
Person.greeting = "Hello!"
//2. 在原型上定义数据成员
Person.prototype.name = 'jake';

(3)静态方法

静态方法通常用于执行不特定于实例的操作,使用static关键字作为前缀,在静态成员中,this引用自身(指类自身)。静态方法适合作为实例工厂。

lass Person {
      constructor(age) {
        this.age_ = age;
      }
      sayAge() {
        console.log(this.age_);
}
static create() {
// 使用随机年龄创建并返回一个 Person 实例
return new Person(Math.floor(Math.random()*100));
} 
}

4. ES6中的继承

ES6中使用extends关键字实现继承,子类可以通过原型链访问到类和原型上定义的方法,而this的值会反映调用相应方法的实例或者类。

(1)super

子类通过super关键字引用它们的原型,其中需要注意以下几点:

  • super关键字只能在子类中使用,且仅限于构造函数、实例方法和静态方法内部

    • 在类构造函数中使用super关键字可以调用父类的构造函数,并传递参数

      class Vehicle {
            constructor() {
              this.hasEngine = true;
            }
      }
      class Bus extends Vehicle {
            constructor() {
        
      // 不要在调用super()之前引用this,否则会抛出ReferenceError 
      super(); // 相当于super.constructor()
      console.log(this instanceof Vehicle);  // true
      console.log(this);                     // Bus { hasEngine: true }
      }
      }
      
    • 在静态方法中可以通过super调用继承的类上的静态方法:

      class Bus extends Vehicle{
      	static identity(){
      		super.identity();
      	}
      }
      
  • 调用super()会调用父类的构造函数,并将返回的实例赋值给this

  • 如果子类中没有定义构造函数,那么实例化子类实例时会调用super(),而且会传入所有给派生类的参数。

  • 在构造函数中使用super时,不能在调用super之前引用this

  • 如果在子类中显示定义了构造函数,要么在构造函数中调用super(),要么必须在其中返回一个对象。

5. 多继承(类混入)

ES6没有显式支持多类继承,但是可以通过现有特性可以轻松模拟这种行为。

可以通过定义一组可嵌套函数来实现,每个函数接收一个父类作为参数,将混入类定义为这个参数的子类,然后返回这个类:

class Vehicle {}
    let FooMixin = (Superclass) => class extends Superclass {
      foo() {
        console.log('foo');
      }
    };
    let BarMixin = (Superclass) => class extends Superclass {
      bar() {
        console.log('bar');
} };
    let BazMixin = (Superclass) => class extends Superclass {
      baz() {
    console.log('baz');
  }
};
function mix(BaseClass, ...Mixins) {
  return Mixins.reduce((accumulator,
}
current) => current(accumulator), BaseClass);
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}

现在很多JS框架已经舍弃了混入模式,专项组合模式: 将方法提取到独立的类和辅助对象中,然后组合起来不使用继承。 也反映了设计原则:“composition over inheritance”

三、补:对象的一些基本知识

1. object.assign()

这个方法接收一个目标对象和一个或者多个源对象作为参数,然后将每个源对象中可枚举和自有的属性复制到目标对象,以字符串和symbol为键的属性会被复制。

  • 实际上对每个对象执行的是浅复制,如果多个源对象都具有相同的值,那么就使用最后一个复制的值。

2. 创建对象

  • 工厂模式(new Object实现,并用函数封装)

    • 可以解决创建多个类似对象的问题,但是没有解决新创建的对象是什么类型的问题。
  • 构造函数模式

    • 此时使用new操作符会执行的步骤同上方使用class一致

3. 理解原型

  • 只要创建一个函数,就会为这个函数创建一个prototype属性,该属性指向了原型对象
  • 默认情况下所有原型对象获得一个constructor属性,指回与之关联的构造函数。
  • 每次调用构造函数创建一个实例,实例内部的隐式原型,即prototype指针(__proto__)都会被赋值为构造函数的原型对象
  • 实例和其构造函数并无直接的指向关系。

截屏2021-09-29 下午4.12.48.png

  • 通过对象来访问属性的时候,会按照属性名称进行查找,查找始于对象实例本身,如果没有找到则会沿着__proto__进入原型对象进行查找。
  • 如果在实例上添加了和原型对象上同名的属性,那么这个属性就会遮蔽(shadow)原型对象上的同名属性(不会修改,但是会屏蔽对其的访问)
  • hasOwnProperty()用于判断一个属性是实例属性还是原型属性。

4.对象迭代

  • 单独使用in进行判断指定属性是否存在时,既查看实例属性又查看原型属性

  • 使用for-in循环,返回所有可以通过对象访问且可以被枚举的属性(包括实例属性和原型属性),遮蔽原型中不可枚举属性的实例属性也会被返回(默认开发者定义的属性都是可枚举的)

  • object.keys()返回该对象所有可枚举的实例属性名称的字符串数组

  • 枚举的顺序也有所差异:

    • for-inobject.keys顺序不确定
    • assign, getOwnPropertyNames(), getOwnPropertySymbols()首先以升序枚举数值键,然后以插入顺序枚举字符串和符号键
  • object.values()返回value数组,object.entries()返回键值对数组。非字符串属性会被转换成字符串属性输出,这两个方法会执行浅复制