目录
文章目录隐藏
  1. JS 中没有真正的类
  2. JS 中的构造函数是什么?
  3. JS 中的“面向类”
  4. 继承
  5. ES6 中的 class 语法

JS 中没有真正的类

JavaScript 和面向类的语言不同,它并没有类来作为对象的抽象模式。JavaScript 中只有对象,而并没有真正的类。JS 只是利用了函数的一种特殊特性——所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象,来模拟类的行为。

要注意的是,如果使用内置的 bind 函数来生成一个硬绑定函数的话,该函数是没有 prototype 属性的,目标函数的 prototype 会代替硬绑定函数的 prototype。在这样的函数上使用 instanceof 或者 new 的话,相当于直接对目标函数使用 。

function Animal() {};
console.log(Animal.prototype);// {}

在 JS 中,new Animal()看起来像是实例化了Animal类,但是事实上并非如此。

const a = new Animal();
console.log(Object.getPrototypeOf(a) === Animal.prototype);// true

在面向类的语言中,类可以被复制(或者说实例化)多次。实例化一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。

但是在 JS 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。new Animal() 会生成一个新对象(我们称之为 a ),这个新对象的内部链接关联的是 Animal.prototype 对象。我们并没有初始化一个类,实际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。

JS 中的构造函数是什么?

function Animal() {};
const a = new Animal();

在 JS 中并没有真正的类,但是在看到这两行代码时,我依然会觉得Animal是一个类。这是为什么呢?在我看来,一个原因在于出现了 new 操作符,而在面向类的语言中,需要使用 new 操作符。另一个原因是,在new Animal()中,Animal调用方式特别像是调用方式很像实例化类时类构造函数的调用方式

但是实际上,Animal和你程序中的其他函数没有任何区别。函数本身并不是构造函数,只是当我们在普通的函数调用前面加上new 关键字之后,就会把这个函数调用变成一个“构造函数调用”(new 会劫持所有普通函数并用构造对象的形式来调用它) 。

简单地说,在 JS 中,“构造函数”可以解释为使用 new 操作符调用的函数。但是,我们需要知道的是,JS 中的函数并不是构造函数,只有使用 new 时,函数调用会变成构造函数调用。

JS 中的“面向类”

function Animal(name) {
    this.name = name;
}
Animal.prototype.sayName = function () {
    console.log(this.name);
};

const dog = new Animal('dog');
const cat = new Animal('cat');

dog.sayName(); // dog
cat.sayName(); // cat
console.log(dog.constructor); //  [Function: Animal]

this.name = name 通过this 的隐式绑定给每个对象都添加了 name 属性,有点像类实例封装的数据值。

Animal.prototype.sayName = ... 会给 Animal.prototype 对象添加一个属性(函数)。在创建的过程中, dog 和 cat 的内 ),这个新对象的内部链接都会关联到 Animal.prototype 上。当 dog和 cat 中无法找到 sayName 时,它会在 Animal.prototype 上找到。

需要注意的是dog.constructor指向了Animal函数,所以dogconstructor属性,似乎在代表着dog是由谁构造的。但是事实上,这仅仅是看起来如此,因为dog本身并没有constructor属性,constructor属性和sayName一样,同样是Animal.prototype 的属性,这个属性和dog(或者cat)之间没有什么联系。对于Animal.prototype而言,constructor也仅仅是Animal函数在声明时所产生的一个默认属性而已(它是不可枚举的,但它是可以更改的),

Animal.prototype.constructor = 'animal';
console.log(dog.constructor); // 'animal'

当改变Animal.prototype的指向时,constructor属性的指向同样变得令人迷惑。这是因为fish上不存在constructor属性,所以查找的是Animal.prototype(即{})上的constructor属性,但是{}也没有constructor属性,所以会继续查找到Object.prototype 。这个对象有 constructor 属性,指向内置的 Object 函数。

Animal.prototype = {};
const fish = new Animal();
console.log(fish.constructor); // [Function: Object]

当然,我们可以手动指定constructor属性。

Animal.prototype = {};
Object.defineProperty(Animal.prototype, 'constructor', {
 enumerable: false, // 不可枚举
 writable: true,
 configurable: true,
 value: Animal, // 让 constructor 指向 Animal
});
const fish = new Animal();
console.log(fish.constructor); // [Function: Animal]

总而言之,constructor属性仅仅是一个普通的,可能会被更改的属性,dog.constructor这种引用是不可靠的。

继承

JS 中原型风格的[继承]

function Animal(name) {
    this.name = name;
}
Animal.prototype.sayName = function () {
    console.log(this.name);
};

function Dog(name, color) {
    Animal.call(this, name);
    this.color = color;
}
// 创建了一个新的 Dog.prototype 对象并关联到 Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
//Object.setPrototypeOf( Dog.prototype, Animal.prototype )
// 注意!现在 Dog.prototype.constructor 的指向已经变为了 Animal
Dog.prototype.sayName = function () {
    console.log('重写 sayName');
    //显式多态,调用 Animal.prototype.sayName
    Animal.prototype.sayName.call(this);
};

const teddy = new Dog('泰迪', '棕色');
teddy.sayName(); // 重写 sayName 泰迪

在声明Dog时,和所有的函数一样,Dog会有一个prototype属性指向默认对象(假设该对象名为originObj),但是originObj并不是我们想要的Foo.prototype。因此我们创建了一个新对象并把这个新对象关联到Foo.prototype,抛弃默认对象originObj。上面代码是通过Object.create实现的。当然也可以通过 ES6 的Object.setPrototypeOf实现,Object.setPrototypeOf( Dog.prototype, Animal.prototype ),这个函数是修改originObj,而不是放弃originObj,也因此通过Object.setPrototypeOf的话,Dog.prototype.constructor指向是没有发生变化的。

我们可以使用instanceof来检查teddyDog或者Animal等的关系。

console.log(teddy instanceof Animal);

instanceof左侧是一个普通的对象 a,右侧是一个函数 B,该操作符会检查 B.prototype 是否存在于 a 的链上。

如果想要检查两个普通对象之间的关系的话,可以使用isPrototypeOf

console.log(Animal.prototype.isPrototypeOf(teddy));

ES6 中的 class 语法

你可能会认为 ES6 的 class 语法是向 JS 中引入了一种新的“类”机制,其实不是这样。 class 基本上只是 (原型链)机制的一种语法糖。

class Animal {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}

class Dog extends Animal {
    constructor(name, color) {
        super(name);
        this.color = color;
    }
    sayName() {
        console.log('重写 sayName');
        //相对多态
        super.sayName();
    }
}

const teddy = new Dog('泰迪', 'brown');
teddy.sayName();

除了语法更好看之外,ES6 还解决了什么问题呢?

  1. 不再引用杂乱的 prototype 了。
  2. Dog声 明 时 直 接“ 继 承 ” 了 Animal, 不 再 需 要 通 过 Object.create来 替
    换 prototype 对象,也不需要设置 __proto__ 或者 Object.setPrototypeOf
  3. 可以通过 super来实现相对多态,这样任何方法都可以引用原型链上层的同名方
    法。不必使用显使多态的写法了,Animal.prototype.sayName.call(this);
  4. class 字面语法不能声明属性(只能声明方法)。看起来这是一种限制,但是它会排除
    掉许多不好的情况,否则,原型链末端的“实例”可能会意外地获取
    其他地方的属性(这些属性隐式被所有“实例”所“共享”)。所以, class 语法实际上
    可以帮助你避免犯错
  5. 可以通过 extends 很自然地扩展对象(子)类型,甚至是内置的对象(子)类型,比如
    Array 或 RegExp 。没有 class ..extends 语法时,想实现这一点是非常困难的。

需要注意的是,superthis不同,super不是动态绑定的。下面的testObj.sayName中的 super 并非指向了它当前的对象testParen,而是指向了Animal

const testObj = {
 name: 'test',
 sayName: Dog.prototype.sayName,
};
const testParen = {
 sayName() {
     console.log('testParen');
 },
};
Object.setPrototypeOf(testObj, testParen);
testObj.sayName();//重写 sayName test

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 聊聊JS中的Class类

发表回复