您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > javascript

深度解读 JS 构造函数、原型、类与继承

时间:2023-12-07 14:42:18  来源:微信公众号  作者:搜狐技术产品
本文深入浅出地讨论了 JAVAScript 构造函数、原型、类、继承的特性和用法,以及它们之间的关系。希望看完本文,能帮助大家对它们有更加清晰通透的认识和掌握!

01、前言

众所周知,JavaScript 是一门面向对象的语言,而构造函数、原型、类、继承都是与对象密不可分的概念。在我们日常前端业务开发中,系统和第三方库已经为我们提供了大部分需要的类,我们的关注点更多是在对象的使用和数据处理上,而比较少需要去自定义构造函数和类,对原型的直接接触就更少了。

 

然而,能深度理解并掌握好构造函数、原型、类与继承,对我们的代码设计大有裨益,也是作为一名高级前端工程师必不可少的基本功。

 

本文旨在用最通俗易懂的解释和简单生动的代码示例,来彻底捋清对象、构造函数、原型、类与继承。我们会以问答对话的形式,层层递进,从构造函数谈起,再引出原型与原型链,分析类为什么是语法糖,最后再推理出 JS 的几种继承方式。

 

在进入正式篇章之前,我们可以先尝试思考以下几个问题:

 

1.new Date().__proto__ == Date.prototype?

2.new Date().constructor == Date?

3.Date.__proto__ == Function.prototype?

4.Function.__proto__ == Function.prototype?

5.Function.prototype.__proto__== Object.prototype?

6.Object.prototype.__proto__ == null?

 

—— 思考分割线 ——

 

没错,它们都是 true !为啥?听我娓娓道来~

02、构造函数

某IT公司前端研发部,新人小Q和职场混迹多年的老白聊起着构造函数、原型与类的话题。

小Q:构造函数我知道呀,平时 new Date(),new Promise() 经常用, Date,Promise 不就是构造函数,我们通过 new 一个构造函数去创建并返回一个新对象。

老白:没错,这些是系统自带的一些构造函数,那你可以自己写个构造函数吗?

小Q:虽然平时用的不多,但也难不倒我~

// 定义个构造函数
function Person(name) {
    this.name = name;
}
// new构造函数,创建对象
let person = new Person("张三");

小Q:看吧 person 就是对象,Person 就是构造函数,清晰明了!

老白:那我要是单纯写这个方法算不算构造函数?

function add(a, b) {
    return a + b;
}

小Q:这不是吧,这明显就是个普通函数啊?

老白:可是它也可以 new 对象哦!

function add(a, b) {
    return a + b;
}
let a = new add(1, 2);
// add {}
console.log(a);
// true
console.log(a instanceof add);
// object
console.log(typeof a);

小Q:诶?

老白:其实所谓构造函数,就是普通函数,关键看你要不要 new 它,但是 new 是在使用的时候,在定义的时候咋知道它后面会不会被 new 呢,所以构造函数只不过是当被用来new 时的称呼。就像你上面的 Person 函数,不要 new 直接运行也是可以的嘛。

function Person(name) {
    this.name = name;
}
Person("张三");

小Q:哦,我懂了,所有函数都可以被 new,都可以作为构造函数咯,所谓构造函数只是一种使用场景。

老白:嗯嗯,总结得很好,但也不全对,比如箭头函数就不能被 new,因为它没有自己的 this 指向,所以不能作为构造函数。比如下面这样就会报错。

let Person = (name) => {
    this.name = name;
};
// Uncaught TypeError: Person is not a constuctor
let person = new Person("张三");

小Q:原来如此,那你刚刚 Person("张三"); ,既然没有创建新对象,那里面的 this 又指向谁了?

老白:这就涉及到函数内 this 指向问题了,可以简单总结以下 5 种场景。

1. 通过 new 调用,this 指向创建的新对象;

2. 直接当做函数调用,this 指向 window(严格模式下为 undefined);

function Person(name) {
    this.name = name;
}
// this 指向 window
Person("张三");
// 张三
console.log(window.name);

(看吧,不注意的话,不小心把 window 对象改了都不知道)

3.作为对象的方法调用,this 指向该对象;

function Person(name) {
    this.name = name;
}
let obj = {
    Person,
};
// this 指向 obj
obj.Person("张三");
// { "name": "张三", Person: f }
console.log(obj);

4.通过 Apply,call,bind 方法,显式指定 this;

function Person(name) {
    this.name = name;
}
// this 指向 call 的第一个参数
Person.call(Math, "张三");
// 张三
console.log(Math.name);

5.箭头函数中没有自己的 this 指向,取决于上下文:

function Person(name) {
    this.name = name;
    
    // 普通函数,this 取决于调用者,即上述的 4 种情况
    setTimeout(function() {
        console.log(this);
    }, 0)
    
    // 箭头函数,this 取决于上下文,我们可以忽略箭头函数的存在
    // 即同上面 this.name = name 中的 this 指向一样
    setTimeout(() => {
        console.log(this);
    }, 0)
}

小Q:原来 this 指向都有这么多种情况,好的,小本本记下了,等下就去试验下。

小Q:等下,我重新看了你的 new add(1, 2),那 a + b = 3 还被 return 了呢,这 3 return 到哪去了?

function add(a, b) {
    return a + b;
}
let a = new add(1, 2);

老白:没错,你注意到了,构造函数是不需要 return 的,函数中的 this 就是创建并返回的新对象了。

但当 new 一个有 return 的构造函数时,如果 return 的是基本类型,则 return 的数据直接被抛弃。

如果 return 一个对象,则最终返回的新对象就是 return 的这个对象,这时原本 this 指向的对象就会被抛弃。

function Person(name) {
    this.name = name;
    // 返回的是对象类型
    return new Date();
}
let person = new Person("张三");
// 返回的是 Date 对象
// Sat Jul 29 2023 16:13:01 GMT+0800 (中国标准时间)
console.log(person);

老白:当然如果要把一个函数的使用用途作为构造函数的话,像我刚刚起名 add() 肯定是不规范的, 一般首字母要大写,并且最好用名词,像你起的 Person 就不错。

小Q:新知识get√

要点归纳

1. 除箭头函数外的所有函数都可以作为构造函数被new

2. 函数内this指向问题

3. 构造函数return问题

4. 构造函数命名规范

03、原型

小Q:都说原型原型,可看了这么久,这代码里也没出现原型呀?

老白:没错,原型是个隐藏的家伙,我们可以通过对象或者构造函数去拿到它。

// 构造函数
function Person(name) {
    this.name = name;
}
// 对象
let person = new Person("张三");

// 通过对象拿到原型(2种方法)
let proto1 = Object.getPrototypeOf(person);
let proto2 = person.__proto__;

// 通过构造函数拿到原型
let proto3 = Person.prototype;

// 验证一下
// true
console.log(proto1 == proto2);
// true
console.log(proto1 == proto3);

小Q:可是这个原型是哪来的呀,我代码里也没创建它呀?

老白:当你声明一个函数时,系统就自动帮你生成了一个关联的原型啦,当然它也是一个普通对象,包含 constructor 字段指向构造函数,并且构造函数的 prototype 属性也会指向这个原型。

当你用构造函数创建对象时,系统又帮你把对象的 __proto__ 属性指向原型。

// 构造函数
function Person(name) {
    this.name = name;
}
// 可以理解为:声明函数时,系统自动执行了下面代码
Person.prototype = {
    // 指向构造函数
    constructor: Person 
}

// 对象
let person = new Person("张三");
// 可以理解为:创建对象时,系统自动执行了下面代码
person.__proto__ == Person.prototype;

小Q:它们的引用关系,稍微有点绕啊~

老白:没事,我画两个图来表示,更加清晰点。

(备注:proto 只是单纯用来表示原型的一个代名而已,代码中并不存在)

深度解读 JS 构造函数、原型、类与继承图片

深度解读 JS 构造函数、原型、类与继承图片

小Q:懂了!

老白:那你说说 {}.__proto__ 和 {}.consrtuctor 分别是什么?

小Q:让我分析下,{} 其实就是 new Object() 的一种字面量写法,本质上就是 Object 对象,那 {}.__proto__ 就是原型 Object.prototype,{}.constructor 就是构造函数 Object,对吧?

老白:没错,只要能熟练掌握上面这个图,构造函数,原型和对象这三者的引用关系基本很清晰了。一开始提的1、2 题基本也迎刃而解了!

  1. new Date().__proto__ == Date.prototype ?
  2. new Date().constructor == Date ?

小Q:那这个原型有什么用呢?

老白:一句话总结:当访问对象的属性不存在时,就会去访问原型的属性。

深度解读 JS 构造函数、原型、类与继承图片

图3

老白:我们可以通过代码验证下,person 对象是没有 age 属性的,所以 person.age 返回的其实是原型的 age 属性值,当原型的 age 属性改变时,person.age 也会跟着改变。

function Person(name) {
    this.name = name;
}
// 给原型增加age属性
Person.prototype.age = 18;

// 对象
let person = new Person("张三");
// 18
console.log(person.age);
// 修改原型的age属性
Person.prototype.age++;
// 19
console.log(person.age);

小Q:那如果我直接 person.age++ 呢,改的是 person 还是原型?

老白:这样的话就相当于 person.age = person.age + 1 啦,等号右边的 person.age 因为  对象目前还没 age 属性,所以拿到的是原型的 age 属性,即18,然后 18 + 1 = 19 将赋值给 person 对象。

后续当你再访问 person.age 时,因为 person 对象已经存在 age 属性了,就不会再检索到原型上了。

这种行为我们一般称为重写,在这个例子里也描述为:person 对象重写了原型上的 age 属性。

深度解读 JS 构造函数、原型、类与继承图片

图4

小Q:那这样的话使用起来岂不是很乱,我还得很小心的分析 person.age 到底是 person 对象的还是原型的?

老白:没错,如果你不想出现这种无意识的重写,将原型上的属性设为对象类型不失为一种办法。

function Person(name) {
    this.name = name;
}
// 原型的info属性是对象
Person.prototype.info = {
    age: 18,
};
let person = new Person("张三");

person.info.age++;

小Q:我懂了,改变的是 info 对象的 age 属性, person 并没有重写 info 属 性,所以 person 对象本身依然没有 info 属性,person.info 依然指向原型。

老白:没错!不过这样也有个坏处,每一个 Person 对象都可以共享原型的 info ,当 info 中的属性被某个对象改变了,也会对其他对象造成影响。

function Person(name) {
    this.name = name;
}
Person.prototype.info = {
    age: 18,
};
let person1 = new Person("张三");
let person2 = new Person("李四");

// person1修改info
person1.info.age = 19;
// person2也会被影响,打印:19
console.log(person2.info.age);

老白:这对我们代码的设计并不好,所以我们一般不在原型上定义数据,而是定义函数,这样对象就可以直接使用挂载在原型上的这些函数了。

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log("hello");
}
let person = new Person("张三");
// hello
person.sayHello();

小Q:我理解了,数据确实不应该被共享,每个对象都应该有自己的数据好点,但是函数无所谓,多个对象可以共享同一个原型函数。

老白:所以你知道为啥 {} 这个对象本身没有任何属性,却可以执行 toString() 方法吗?

小Q:【恍然大悟】来自它的原型 Object.prototype !

老白:不仅如此,很多系统自带的构造函数产生的对象,其方法都是挂载在原型上的。比如我们经常用的数组方法,你以为是数组对象自己的方法吗?不,是数组原型 Array.prototype 的方法,我们可以验证下。

let array = [];
// array对象的push和原型上的push是同一个
// 打印:true
console.log(array.push == Array.prototype.push);
// array对象本身没有自己的push属性
// 打印:false
console.log(array.hasOwnProperty("push"));

深度解读 JS 构造函数、原型、类与继承图片

小Q:【若有所思】

老白:再比如,你随便定义一个函数 function fn() {},为啥它就能 fn.call() 这样执行呢,它的 call 属性是哪来的?

小Q:来自它的原型?函数其实是 Function 的对象,那它的原型就是 Function.prototype,试验一下。

function fn() {}
// true
console.log(fn.constructor == Function);
// true
console.log(fn.call == Function.prototype.call);

老白:回答正确。在实际开发中,我们也可以通过修改原型上的函数,来改变对象的函数执行。比如说我们修改数组原型的 push 方法,加个监听,这样所有数组对象执行 push 方法时就能被监听到了。

Array.prototype.push = (function (push) {
    // 闭包,push是原始的那个push方法
    return function (...items) {
        
        // 执行push要指定this
        push.call(this, ...items);
        
        console.log("监听push完成,执行一些操作");
    };
})(Array.prototype.push);

let array = [];
// 打印:监听push完成,执行一些操作
array.push(1, 2);
// 打印:[1, 2]
console.log(array);

老白:不只修改,也可以新增,比如说某些旧版浏览器数组不支持 includes 方法,那我们就可以在原型上新增一个 includes 属性,保证代码中数组对象使用 includes() 不会报错(这也是 Polyfill.js 的目的)。

// 没有includes
if(!Array.prototype.includes) {
    Array.prototype.includes = function() {
        // 自己实现includes
    }
}

小Q:又又涨知识了~

老白:原型相关的也说的差不多了,结合刚刚讨论的构造函数,考你一个:手写一个 new 函数。

小Q:啊啊,提示一下?

老白:好,我们简单分析一下 new 都做了什么

  1. 创建一个对象,绑定原型;
  2. 以这个对象为 this 指向执行构造函数。

小Q:我试试~

function myNew(Fn, ...args) {
    var obj = {
        __proto__: Fn.prototype,
    };
    Fn.apply(obj, args);
    return obj;
}

小Q:试验通过!

// 构造函数
function Person(name) {
    this.name = name;
}
// 原型
Person.prototype.age = 18;
// 创建对象
let person = myNew(Person, "张三");

// Person {name: "张三"}
console.log(person);
// 18
console.log(person.age);

老白:不错不错,让我帮你再稍微完善一下嘿嘿~

function myNew(Fn, ...args) {
    // 通过Object.create指定原型,更加符合规范
    var obj = Object.create(Fn.prototype);
    
    // 指定this为obj对象,执行构造函数
    let result = Fn.apply(obj, args);
    
    // 判断构造函数的返回值是否是对象
    return result instanceof Object ? result : obj;
}

 

要点归纳

1. 对象,构造函数,原型三者的引用关系

2. 原型的定义,特性及用法

3. 手写new函数

 

04、原型链

老白:刚刚我们说当访问对象的属性不存在时,就会去访问原型的属性,那假如原型上的属性也不存在呢?

小Q:返回 undefined?

老白:不对哦,原型本身也是一个对象,它也有它自己的原型。所以当访问一个对象的属性不存在时,就会检索它的原型,检索不到就继续往上检索原型的原型,一直检索到根原型 Object.prototype,如果还没有,才会返回 undefined,这也称为原型链。

深度解读 JS 构造函数、原型、类与继承图片

小Q:原来如此,所以说所有的对象都可以使用根原型 Object.prototype 上定义的方法咯。

老白:没错,不过有一些原型会重写根原型上的方法,就比如 toString(),在 Date.prototype,Array.prototype 中都会有它们自己的定义。

// [object Object]
console.log({}.toString())

// 1,2,3
console.log([1,2,3].toString())

// Tue Aug 01 2023 17:58:05 GMT+0800 (中国标准时间)
console.log(new Date().toString())

小Q:理解了原型链,看回开始的3~6题,好像也不难了。

Date、Function 的原型是 Function.prototype,第 3、4 题就解了。

Function.prototype 的原型是 Object.prototype,第 5 题也解了。

Object.prototype 是根原型,所以它的 __proto__ 属性就为 null,第 6 题也解了。

  1. Date.__proto__ == Function.prototype ?
  2. Function.__proto__ == Function.prototype ?
  3. Function.prototype.__proto__== Object.prototype ?
  4. Object.prototype.__proto__ == null ?

老白:完全正确。最后再考你一道和原型链相关的题,手写 instanceOf 函数。提示一下,instanceOf 的原理是判断构造函数的 prototype 属性是否在对象的原型链上。

// array的原型链:Array.prototype → Object.prototype
let array = [];
// true
console.log(array instanceof Array);
// true
console.log(array instanceof Object);
// false
console.log(array instanceof Function);

小Q:好了嘞~

function myInstanceof(obj, Fn) {
    while (true) {
        obj = obj.__proto__; 
        // 匹配上了
        if (obj == Fn.prototype) {
            return true;
        }
        // 到达原型链的尽头了
        if (obj == null) {
            return false;
        }
    }
}

检测一下:

let array = [];
// true
console.log(myInstanceof(array, Array));
// true
console.log(myInstanceof(array, Object));
// false
console.log(myInstanceof(array, Function));

老白:Good!

要点归纳

1. 原型链

2. 手写 instanceOf函数

05、类

小Q:好不容易把构造函数和原型都弄懂,怎么 ES6 又推出类呀,学不动了 T_T。

老白:不慌,类其实只是种语法糖,本质上还是”构造函数+原型“。

我们先看一下类的语法,类中可以包含有以下4种写法不同的元素。

  • 对象属性:key = xx
  • 原型属性:key() {}
  • 静态属性:static key = x 或 static key() {}
  • 构造器:constructor() {}
class Person {
    // 对象属性
    a = "a";
    b = function () {
        console.log("b");
    };
    // 原型属性
    c() {
        console.log("c");
    }
    // 构造器
    constructor() {
        // 修改对象属性
        this.a = "A";
        // 新增对象属性
        this.d = "d";
    }
    // 静态属性
    static e = "e";
    static f() {
        console.log("f");
    }
}

我们再将这种 class 语法糖写法还原成构造函数写法。

function Person() {
    // 对象属性
    this.a = "a";
    this.b = function () {
        console.log("b");
    };
    // 构造器
    this.a = "A";
    this.d = "d";
}

// 原型属性
Person.prototype.c = function () {
    console.log("c");
};

// 静态属性
Person.e = "e";
Person.f = function () {
    console.log("f");
};

通过下面一些方法检测,上面的2种写法会得到同样的结果。

// Person类本质是个构造函数,打印:function
console.log(typeof Person);

// Person的静态属性,打印:e
console.log(Person.e);

// 可以看到原型属性c,打印:{constructor: ƒ, c: ƒ}
console.log(Person.prototype);

let person = new Person();

// 可以看到对象属性a b d,打印:Person {a: 'A', d: 'd', b: ƒ}
console.log(person);
// 对象的构造函数就是Person,打印:true
console.log(person.constructor == Person);

小Q:所以类只不过是将本来比较繁琐的构造函数的写法给简化了而已,这语法糖果然甜~

小Q:不过我发现一个问题,在 class 写法中的原型属性只能是函数,不能是数据?

老白:没错,这也呼应了前面说的,原型上只推荐定义函数,不推荐定义数据,避免不同对象共享同一个数据。

要点归纳

1. 类的语法

2. 类还原成构造函数写法

06、继承

小Q:我又又发现了一个问题,ES6 的 class 还可以 extends 另一个类呢,这也是语法糖?

老白:没错,这就是继承,但是要弄懂 ES6 的这套继承是怎么来的,还得从最开始的继承方式说起。所谓继承,就是我们是希望子类可以拥有父类的属性方法,这和上面谈到的原型的特性有点不谋而合。

我们用一个例子来思考思考,有这么 2 个类,如何让 Cat 继承 Animal,使得 Cat 的对象也有 type 属性呢?

// 父类
function Animal() {
    this.type = "动物";
}
// 子类
function Cat() {
    this.name = "猫";
}

小Q:让 Animal 对象充当 Cat 的原型!

function Animal() {
    this.type = "动物";
}
function Cat() {
    this.name = "猫";
}
// 指定Cat的原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

let cat = new Cat();
// Cat对象拥有了Animal的属性
console.log(cat.type);

老白:没错,这是我们学完原型之后,最直观的一种继承实现方式,这种继承又叫原型链式继承。但是这种继承方式存在 2 个缺点:

  1. 父类对象作为原型,其属性会被所有子类对象共享;
  2. 创建子类对象时无法向父类构造函数传参。
function Animal(type) {
    this.type = type;
}
function Cat(type) {
    this.name = "猫";
}
// 在这里就已经创建了Animal对象
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 创建子类对象时无法向父类构造函数传参
let cat = new Cat("哺乳动物");

// type属性来自原型,被所有Cat对象共享,打印:undefined
console.log(cat.type);

小Q:我想到个办法,可以一举解决上面2个缺点。

在子类构造函数中执行父类构造函数,并且指定执行父类构造函数中的 this 是子类对象,这样属性就都是属于子类对象本身了,不存在共享。同时在创建子类对象时,也可以给父类构造函数传参了,一举两得。

function Animal(type) {
    this.type = type;
}
function Cat(type) {
    // 执行父类,显式指定this就是子类的对象
    Animal.call(this, type);
    this.name = "猫";
}
let cat = new Cat("哺乳动物");

// Cat {type: '哺乳动物', name: '猫'}
console.log(cat);

老白:这种继承方式叫 构造函数式继承,确实解决了 原型链式继承 带来的问题,不过这种继承方式因为没有用到原型,又有产生了2个新的问题:

  1. 没有继承父类原型的属性方法;
  2. 子类对象不是父类的实例。
function Animal(type) {
    this.type = type;
}
// 父类的原型方法
Animal.prototype.eat = function () {
    console.log("吃");
};
function Cat(type) {
    Animal.call(this, type);
    this.name = "猫";
}
let cat = new Cat("哺乳动物");

// 没有继承父类原型的属性方法,打印:undefined
console.log(cat.eat);
// 子类对象不是父类的实例,打印:false
console.log(cat instanceof Animal);

小Q:看来还要再改进,不如我把 原型链式 和 构造函数式 这 2 种继承方式都用上,让它们互补。

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function () {
    console.log("吃");
};
// 子类构造函数
function Cat(type) {
    Animal.call(this, type);
    this.name = "猫";
}
// 父类对象充当子类原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

试验一下,果然所有问题都解决了。

// 可以给父类构造函数传参
let cat = new Cat("哺乳动物");

// 子类对象拥用自己属性,而非来自原型,避免数据共享
// 打印:Cat {type: '哺乳动物', name: '猫'}
console.log(cat);

// 子类对象可以继承到父类原型的方法,打印:吃
cat.eat();

// 子类对象属于父类的实例,打印:true
console.log(cat instanceof Animal);

老白:非常聪明,你又道出了第三种继承方式,组合式继承。即 原型链式 + 构造函数式 = 组合式。问题确实都解决了,但是有没有发现,这种方式执行了 2 遍父类构造函数。

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function () {
    console.log("吃");
};
function Cat(type) {
    // 第二次执行父类构造函数
    Animal.call(this, type);
    this.name = "猫";
}
// 第一次执行父类构造函数
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

小Q:多执行了一遍,确实不够完美,这怎么搞?

老白:其实关键在 Cat.prototype = new Animal(),你只不过想让子类对象也能继承到父类的原型,而这里创建了一个父类对象,为啥?说到底还是利用原型链: 子类对象 → 父类对象 → 父类原型。

如果我们不要中间那个"父类对象",而是用一个“空对象x”替换,让原型链变成:子类对象 → 空对象x → 父类原型,这样也能达到目的,就不用执行那遍没必要的父类构造函数了。

// 组合式继承:创建父类对象做子类原型
let animal = new Animal();
Cat.prototype = animal;

// 改进:创建一个空对象做子类原型,并且这个空对象的原型是父类原型
let x = Object.create(Animal.prototype);
Cat.prototype = x;

小Q:妙啊,这回完美了。

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function () {
    console.log("吃");
};
function Cat(type) {
    Animal.call(this, type);
    this.name = "猫";
}
// 寄生组合式,改进了组合式,少执行了一遍没必要的父类构造函数
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

老白:这种继承方式又叫 寄生组合式继承,相当于在组合式继承的基础上进一步优化。回顾上面的几种继承方式的演变过程,原型链式 → 构造函数式 → 组合式 → 寄生组合式, 其实就是不断优化的过程,最终我们才推理出比较完美的继承方式。

小Q:那 ES6 class 的 extends 继承 又是怎样呢?

老白:说到底就是 寄生组合式继承 的语法糖。我们先看看它的语法。

class Animal {
    eat() {
        console.log("吃");
    }
    constructor(type) {
        this.type = type;
    }
}
// Cat继承Animal
class Cat extends Animal {
    constructor(type) {
        // 执行父类构造函数,相当于 Animal.call(this, type);
        super(type);
        
        // 执行完super(),子类对象就有父类属性了,打印:哺乳动物
        console.log(this.type);
        
        this.name = "猫";
    }
}

创建对象试验一下:

let cat = new Cat("哺乳动物");

// 子类原型的原型就是父类原型,打印:true
console.log(Cat.prototype.__proto__ == Animal.prototype);

// 子类本身拥有父类的属性,打印:Cat {type: '哺乳动物', name: '猫'}
console.log(cat);

打印的结果展示的特性和 寄生组合式 是一样的:

  1. 子类原型的原型就是父类原型;
  2. 子类本身拥有父类的属性。

特性1 可以理解为 extends 背地里执行了:

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

特性2 在于 super(),它相当于 Animal.call(this),执行 super() 就是执行父类构造函数,将原本父类中的属性都赋值给子类对象。

在 ES6 的语法中还要求 super() 必须在 this 的使用前调用,也是为了保证父类构造函数先执行,避免在子类构造器中设置的 this 属性被父类构造函数覆盖。

class Animal {
    constructor() {
        // 假如不报错,this.name = "猫" 就被 this.name= "狗" 覆盖了
        this.name = "狗";
    }
}
class Cat extends Animal {
    constructor(type) {
        this.name = "猫";
        // 没有在this使用前调用,报错
        super();
    }
}

小Q:看懂 寄生组合式继承, extends 继承 就是小菜一碟呀~

老白:最后再补充一下 super 的语法,可以子类的静态属性方法中通过 super.xx 访问父类静态属性方法。

class Animal {
    constructor() {}
    static num = 1;
    static say() {
        console.log("hello");
    }
}
class Cat extends Animal {
    constructor() {
        super();
    }
    // super.num 相当于 Animal.num
    static count = super.num + 1;
    
    static talk() {
        // super.say() 相当于 Animal.say()
        super.say();
    }
}
// 2
console.log(Cat.count);
// hello
Cat.talk();

 super 是一个语法糖的特殊关键词,特殊用法,并不指向某个对象,不能单独使用,以下情况都是不允许的。

class Animal {}
class Cat extends Animal {
    constructor() {
        // 报错
        let _super = super;
        // 报错
        console.log(super);
    }
    static talk() {
        // 报错
        console.log(super);
    }
}

要点归纳

  1. 原型链式继承
  2. 构造函数式继承
  3. 组合式继承
  4. 寄生组合式继承
  5. extends 继承

07、总结

本文深入浅出地讨论了 JavaScript 构造函数、原型、类、继承的特性和用法,以及它们之间的关系。希望看完本文,能帮助大家对它们有更加清晰通透的认识和掌握!



Tags:JS   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
又出新JS运行时了!JS运行时大盘点
Node.js是基于Google V8引擎的JavaScript运行时,以非阻塞I/O和事件驱动架构为特色,实现全栈开发。它跨平台且拥有丰富的生态系统,但也面临安全性、TypeScript支持和性能等挑战...【详细内容】
2024-03-21  Search: JS  点击:(22)  评论:(0)  加入收藏
GitHub顶流"Web OS"——运行于浏览器的桌面操作系统、用户超100万、原生jQuery和JS编写
Puter 是近日在 GitHub 上最受欢迎的一款开源项目,正式开源还没到一周 ——star 数就已接近 7k。作者表示这个项目已开发 3 年,并获得了超过 100 万用户。根据介绍,P...【详细内容】
2024-03-10  Search: JS  点击:(17)  评论:(0)  加入收藏
前端开始“锈化”?Vue团队开源JS打包工具:基于Rust、速度极快、尤雨溪主导
Vue 团队已正式开源Rolldown —— 基于 Rust 的 JavaScrip 打包工具。Rolldown 是使用 Rust 开发的 Rollup 替代品,它提供与 Rollup 兼容的应用程序接口和插件接口...【详细内容】
2024-03-09  Search: JS  点击:(11)  评论:(0)  加入收藏
如何在Rust中操作JSON,你学会了吗?
sonic-rs ​还具有一些额外的方法来进行惰性评估和提高速度。例如,如果我们想要一个 JSON​ 字符串文字,我们可以在反序列化时使用 LazyValue​ 类型将其转换为一个仍然带有斜...【详细内容】
2024-02-27  Search: JS  点击:(47)  评论:(0)  加入收藏
JS小知识,使用这6个小技巧,避免过多的使用 if 语句
最近在重构我的代码时,我注意到早期的代码使用了太多的 if 语句,达到了我以前从未见过的程度。这就是为什么我认为分享这些可以帮助我们避免使用过多 if 语句的简单技巧很重要...【详细内容】
2024-01-30  Search: JS  点击:(56)  评论:(0)  加入收藏
花 15 分钟把 Express.js 搞明白,全栈没有那么难
Express 是老牌的 Node.js 框架,以简单和轻量著称,几行代码就可以启动一个 HTTP 服务器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都与 Express 息息相关。Express 框...【详细内容】
2024-01-16  Search: JS  点击:(85)  评论:(0)  加入收藏
JS 中如何克隆对象?你学会了吗?
大家好,这里是大家的林语冰。JS 中如何克隆对象?此问题看似简单,实际十分复杂。假设我们需要获取下述对象的拷贝。const cat = { name: '薛定谔', girlFriends: { na...【详细内容】
2024-01-05  Search: JS  点击:(102)  评论:(0)  加入收藏
理解 Node.js 中的事件循环
你已经使用 Node.js 一段时间了,构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但是有些事情一直在困扰着你——事件循环(Event Loop)。如果你像我...【详细内容】
2024-01-05  Search: JS  点击:(110)  评论:(0)  加入收藏
彻底搞懂 JS 类型转换
1. 什么是类型转换?Javascript 是一种弱类型语言,这意味着变量是没有明确类型的,而是由 JavaScript 引擎在编译时隐式完成。类型转换就是将一种数据类型转换为另一种数据类型,例...【详细内容】
2024-01-03  Search: JS  点击:(103)  评论:(0)  加入收藏
.NET配置文件大揭秘:轻松读取JSON、XML、INI和环境变量
概述:.NET中的IConfiguration接口提供了一种多源读取配置信息的灵活机制,包括JSON、XML、INI文件和环境变量。通过示例,清晰演示了从这些不同源中读取配置的方法,使配置获取变得...【详细内容】
2023-12-28  Search: JS  点击:(92)  评论:(0)  加入收藏
▌简易百科推荐
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  前端新世界  微信公众号  Tags:JavaScript   点击:(4)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践——如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  前端新世界  微信公众号  Tags:JavaScript   点击:(25)  评论:(0)  加入收藏
又出新JS运行时了!JS运行时大盘点
Node.js是基于Google V8引擎的JavaScript运行时,以非阻塞I/O和事件驱动架构为特色,实现全栈开发。它跨平台且拥有丰富的生态系统,但也面临安全性、TypeScript支持和性能等挑战...【详细内容】
2024-03-21  前端充电宝  微信公众号  Tags:JS   点击:(22)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  前端历险记  微信公众号  Tags:JavaScript   点击:(20)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  WangLiwen    Tags:JavaScript   点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  京东云开发者    Tags:JavaScript   点击:(2)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06    51CTO  Tags:JavaScript   点击:(52)  评论:(0)  加入收藏
JS小知识,使用这6个小技巧,避免过多的使用 if 语句
最近在重构我的代码时,我注意到早期的代码使用了太多的 if 语句,达到了我以前从未见过的程度。这就是为什么我认为分享这些可以帮助我们避免使用过多 if 语句的简单技巧很重要...【详细内容】
2024-01-30  前端达人  今日头条  Tags:JS   点击:(56)  评论:(0)  加入收藏
18个JavaScript技巧:编写简洁高效的代码
本文翻译自 18 JavaScript Tips : You Should Know for Clean and Efficient Code,作者:Shefali, 略有删改。在这篇文章中,我将分享18个JavaScript技巧,以及一些你应该知道的示例...【详细内容】
2024-01-30  南城大前端  微信公众号  Tags:JavaScript   点击:(65)  评论:(0)  加入收藏
使用 JavaScript 清理我的 200GB iCloud,有了一个意外发现!
本文作者在综合成本因素之下,决定用 Java 脚本来清理一下自己的 iCloud,结果却有了一个意外发现,即在 iCloud 中上传同一个视频和删除此视频之后,iCloud 的空间并不一致,这到底是...【详细内容】
2024-01-11    CSDN  Tags:JavaScript   点击:(97)  评论:(0)  加入收藏
站内最新
站内热门
站内头条