眾所周知,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 !為啥?聽我娓娓道來~
某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);// trueconsole.log(a instanceof add);// objectconsole.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 constuctorlet person = new Person("張三");
小Q:原來如此,那你剛剛 Person("張三"); ,既然沒有創建新對象,那里面的 this 又指向誰了?
老白:這就涉及到函數內 this 指向問題了,可以簡單總結以下 5 種場景。
1. 通過 new 調用,this 指向創建的新對象;
2. 直接當做函數調用,this 指向 window(嚴格模式下為 undefined);
function Person(name) { this.name = name;}// this 指向 windowPerson("張三");// 張三console.log(window.name);
(看吧,不注意的話,不小心把 window 對象改了都不知道)
3.作為對象的方法調用,this 指向該對象;
function Person(name) { this.name = name;}let obj = { Person,};// this 指向 objobj.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. 構造函數命名規范 |
小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;// 驗證一下// trueconsole.log(proto1 == proto2);// trueconsole.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 只是單純用來表示原型的一個代名而已,代碼中并不存在)
圖片
圖片
小Q:懂了!
老白:那你說說 {}.__proto__ 和 {}.consrtuctor 分別是什么?
小Q:讓我分析下,{} 其實就是 new Object() 的一種字面量寫法,本質上就是 Object 對象,那 {}.__proto__ 就是原型 Object.prototype,{}.constructor 就是構造函數 Object,對吧?
老白:沒錯,只要能熟練掌握上面這個圖,構造函數,原型和對象這三者的引用關系基本很清晰了。一開始提的1、2 題基本也迎刃而解了!
小Q:那這個原型有什么用呢?
老白:一句話總結:當訪問對象的屬性不存在時,就會去訪問原型的屬性。
圖片
圖3
老白:我們可以通過代碼驗證下,person 對象是沒有 age 屬性的,所以 person.age 返回的其實是原型的 age 屬性值,當原型的 age 屬性改變時,person.age 也會跟著改變。
function Person(name) { this.name = name;}// 給原型增加age屬性Person.prototype.age = 18;// 對象let person = new Person("張三");// 18console.log(person.age);// 修改原型的age屬性Person.prototype.age++;// 19console.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 屬性。
圖片
圖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修改infoperson1.info.age = 19;// person2也會被影響,打印:19console.log(person2.info.age);
老白:這對我們代碼的設計并不好,所以我們一般不在原型上定義數據,而是定義函數,這樣對象就可以直接使用掛載在原型上的這些函數了。
function Person(name) { this.name = name;}Person.prototype.sayHello = function() { console.log("hello");}let person = new Person("張三");// helloperson.sayHello();
小Q:我理解了,數據確實不應該被共享,每個對象都應該有自己的數據好點,但是函數無所謂,多個對象可以共享同一個原型函數。
老白:所以你知道為啥 {} 這個對象本身沒有任何屬性,卻可以執行 toString() 方法嗎?
小Q:【恍然大悟】來自它的原型 Object.prototype !
老白:不僅如此,很多系統自帶的構造函數產生的對象,其方法都是掛載在原型上的。比如我們經常用的數組方法,你以為是數組對象自己的方法嗎?不,是數組原型 Array.prototype 的方法,我們可以驗證下。
let array = [];// array對象的push和原型上的push是同一個// 打印:trueconsole.log(array.push == Array.prototype.push);// array對象本身沒有自己的push屬性// 打印:falseconsole.log(array.hasOwnProperty("push"));
圖片
小Q:【若有所思】
老白:再比如,你隨便定義一個函數 function fn() {},為啥它就能 fn.call() 這樣執行呢,它的 call 屬性是哪來的?
小Q:來自它的原型?函數其實是 Function 的對象,那它的原型就是 Function.prototype,試驗一下。
function fn() {}// trueconsole.log(fn.constructor == Function);// trueconsole.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 的目的)。
// 沒有includesif(!Array.prototype.includes) { Array.prototype.includes = function() { // 自己實現includes }}
小Q:又又漲知識了~
老白:原型相關的也說的差不多了,結合剛剛討論的構造函數,考你一個:手寫一個 new 函數。
小Q:啊啊,提示一下?
老白:好,我們簡單分析一下 new 都做了什么
小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);// 18console.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函數 |
老白:剛剛我們說當訪問對象的屬性不存在時,就會去訪問原型的屬性,那假如原型上的屬性也不存在呢?
小Q:返回 undefined?
老白:不對哦,原型本身也是一個對象,它也有它自己的原型。所以當訪問一個對象的屬性不存在時,就會檢索它的原型,檢索不到就繼續往上檢索原型的原型,一直檢索到根原型 Object.prototype,如果還沒有,才會返回 undefined,這也稱為原型鏈。
圖片
小Q:原來如此,所以說所有的對象都可以使用根原型 Object.prototype 上定義的方法咯。
老白:沒錯,不過有一些原型會重寫根原型上的方法,就比如 toString(),在 Date.prototype,Array.prototype 中都會有它們自己的定義。
// [object Object]console.log({}.toString())// 1,2,3console.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 題也解了。
老白:完全正確。最后再考你一道和原型鏈相關的題,手寫 instanceOf 函數。提示一下,instanceOf 的原理是判斷構造函數的 prototype 屬性是否在對象的原型鏈上。
// array的原型鏈:Array.prototype → Object.prototypelet array = [];// trueconsole.log(array instanceof Array);// trueconsole.log(array instanceof Object);// falseconsole.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 = [];// trueconsole.log(myInstanceof(array, Array));// trueconsole.log(myInstanceof(array, Object));// falseconsole.log(myInstanceof(array, Function));
老白:Good!
要點歸納 |
1. 原型鏈 |
2. 手寫 instanceOf函數 |
小Q:好不容易把構造函數和原型都弄懂,怎么 ES6 又推出類呀,學不動了 T_T。
老白:不慌,類其實只是種語法糖,本質上還是”構造函數+原型“。
我們先看一下類的語法,類中可以包含有以下4種寫法不同的元素。
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類本質是個構造函數,打印:functionconsole.log(typeof Person);// Person的靜態屬性,打印:econsole.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,打印:trueconsole.log(person.constructor == Person);
小Q:所以類只不過是將本來比較繁瑣的構造函數的寫法給簡化了而已,這語法糖果然甜~
小Q:不過我發現一個問題,在 class 寫法中的原型屬性只能是函數,不能是數據?
老白:沒錯,這也呼應了前面說的,原型上只推薦定義函數,不推薦定義數據,避免不同對象共享同一個數據。
要點歸納 |
1. 類的語法 |
2. 類還原成構造函數寫法 |
小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 個缺點:
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對象共享,打印:undefinedconsole.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個新的問題:
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("哺乳動物");// 沒有繼承父類原型的屬性方法,打印:undefinedconsole.log(cat.eat);// 子類對象不是父類的實例,打印:falseconsole.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();// 子類對象屬于父類的實例,打印:trueconsole.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繼承Animalclass Cat extends Animal { constructor(type) { // 執行父類構造函數,相當于 Animal.call(this, type); super(type); // 執行完super(),子類對象就有父類屬性了,打印:哺乳動物 console.log(this.type); this.name = "貓"; }}
創建對象試驗一下:
let cat = new Cat("哺乳動物");// 子類原型的原型就是父類原型,打印:trueconsole.log(Cat.prototype.__proto__ == Animal.prototype);// 子類本身擁有父類的屬性,打印:Cat {type: '哺乳動物', name: '貓'}console.log(cat);
打印的結果展示的特性和 寄生組合式 是一樣的:
特性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(); }}// 2console.log(Cat.count);// helloCat.talk();
super 是一個語法糖的特殊關鍵詞,特殊用法,并不指向某個對象,不能單獨使用,以下情況都是不允許的。
class Animal {}class Cat extends Animal { constructor() { // 報錯 let _super = super; // 報錯 console.log(super); } static talk() { // 報錯 console.log(super); }}
要點歸納
本文深入淺出地討論了 JavaScript 構造函數、原型、類、繼承的特性和用法,以及它們之間的關系。希望看完本文,能幫助大家對它們有更加清晰通透的認識和掌握!
本文鏈接:http://www.www897cc.com/showinfo-26-39542-0.html深度解讀 JS 構造函數、原型、類與繼承
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com