一、前言
在面試過程中,頻頻被原型相關知識問住,每次回答都支支吾吾。後來有家非常心儀的公司,在二面時,果不其然,又問原型了!
我痛下決心用了兩天時間鑽研了下原型,弄明白後發現世界都明亮了,原來這麼簡單 ~
有些理解還比較淺薄,隨著時間的推移和理解的深入,以後還會補充。如果大家發現我理解的有問題,歡迎大家在評論中指正。話不多說,切入正題。
二、建構函式
講原型則離不開建構函式,讓我們先來認識下建構函式。
2.1 建構函式分為 例項成員 和 靜態成員
讓我們先來看看他們分別是什麼樣子的。
例項成員: 例項成員就是在建構函式內部,透過this新增的成員。例項成員只能透過例項化的物件來訪問。
靜態成員: 在建構函式本身上新增的成員,只能透過建構函式來訪問
function Star(name,age) {
//例項成員
this.name = name;
this.age = age;
}
//靜態成員
Star.sex = '女';
let stars = new Star('小紅',18);
console.log(stars); // Star {name: "小紅", age: 18}
console.log(stars.sex); // undefined 例項無法訪問sex屬性
console.log(Star.name); //Star 透過建構函式無法直接訪問例項成員
console.log(Star.sex); //女 透過建構函式可直接訪問靜態成員
2.2 透過建構函式建立物件
該過程也稱作例項化
2.2.1 如何透過建構函式建立一個物件?
function Father(name) {
this.name = name;
}
let son = new Father('Lisa');
console.log(son); //Father {name: "Lisa"}
此時,son就是一個新物件。
2.2.2 new一個新物件的過程,發生了什麼?
(1) 建立一個空物件 son {}
(2) 為 son 準備原型鏈連線 son.__proto__ = Father.prototype
(3) 重新繫結this,使建構函式的this指向新物件 Father.call(this)
(4) 為新物件屬性賦值 son.name
(5) 返回this return this
,此時的新物件就擁有了建構函式的方法和屬性了
2.2.3 每個例項的方法是共享的嗎?
這要看我們如何定義該方法了,分為兩種情況。
方法1:在建構函式上直接定義方法(不共享)
function Star() {
this.sing = function () {
console.log('我愛唱歌');
}
}
let stu1 = new Star();
let stu2 = new Star();
stu1.sing();//我愛唱歌
stu2.sing();//我愛唱歌
console.log(stu1.sing === stu2.sing);//false
很明顯,stu1 和 stu2 指向的不是一個地方。
所以 在建構函式上透過this來新增方法的方式來生成例項,每次生成例項,都是新開闢一個記憶體空間存方法。這樣會導致記憶體的極大浪費,從而影響效能。
方法2:透過原型新增方法(共享)
建構函式透過原型分配的函式,是所有物件共享的。
function Star(name) {
this.name = name;
}
Star.prototype.sing = function () {
console.log('我愛唱歌', this.name);
};
let stu1 = new Star('小紅');
let stu2 = new Star('小藍');
stu1.sing();//我愛唱歌 小紅
stu2.sing();//我愛唱歌 小藍
console.log(stu1.sing === stu2.sing);//true
2.2.4 例項的屬性為基本型別是,它們是共享的嗎?
屬性儲存的是如果儲存的是基本型別,不存在共享問題,是否相同要看值內容。
let stu1 = new Star('小紅');
let stu2 = new Star('小紅');
console.log(stu1.name === stu2.name);//true
let stu1 = new Star('小紅');
let stu2 = new Star('小藍');
console.log(stu1.name === stu2.name);//false
2.2.5 定義建構函式的規則
公共屬性定義到建構函式里面,公共方法我們放到原型物件身上。
三、原型
前面我們在 例項化 和 例項共享方法 時,都提到了原型。那麼現在聊聊這個神秘的原型到底是什麼?
3.1 什麼是原型?
Father.prototype 就是原型,它是一個物件,我們也稱它為原型物件。
3.2 原型的作用是什麼?
原型的作用,就是共享方法。
我們透過 Father.prototype.method
可以共享方法,不會反應開闢空間儲存方法。
3.3 原型中this的指向是什麼?
原型中this的指向是例項。
四、原型鏈
4.1 什麼是原型鏈?
原型與原型層層相連結的過程即為原型鏈。
4.2 原型鏈應用
物件可以使用建構函式prototype原型物件的屬性和方法,就是因為物件有__proto__原型的存在
每個物件都有__proto__原型的存在
參考 前端進階面試題詳細解答
function Star(name,age) {
this.name = name;
this.age = age;
}
Star.prototype.dance = function(){
console.log('我在跳舞',this.name);
};
let obj = new Star('張萌',18);
console.log(obj.__proto__ === Star.prototype);//true
4.3 原型鏈圖
4.4 原型查詢方式
例如:查詢obj的dance方法
function Star(name) {
this.name = name;
(1)首先看obj物件身上是否有dance方法,如果有,則執行物件身上的方法
this.dance = function () {
console.log(this.name + '1');
}
}
(2)如果沒有dance方法,就去建構函式原型物件prototype身上去查詢dance這個方法。
Star.prototype.dance = function () {
console.log(this.name + '2');
};
(3)如果再沒有dance方法,就去Object原型物件prototype身上去查詢dance這個方法。
Object.prototype.dance = function () {
console.log(this.name + '3');
};
(4)如果再沒有,則會報錯。
let obj = new Star('小紅');
obj.dance();
(1)首先看obj物件身上是否有dance方法,如果有,則執行物件身上的方法。
(2)如果沒有dance方法,就去建構函式原型物件prototype身上去查詢dance這個方法。
(3)如果再沒有dance方法,就去Object原型物件prototype身上去查詢dance這個方法。
(4)如果再沒有,則會報錯。
4.5 原型的構造器
原型的構造器指向建構函式。
function Star(name) {
this.name = name;
}
let obj = new Star('小紅');
console.log(Star.prototype.constructor === Star);//true
console.log(obj.__proto__.constructor === Star); //true
4.5.1 在原型上新增方法需要注意的地方
方法1:建構函式.prototype.方法
在原型物件上直接新增方法,此時的原型物件是有constructor
構造器的,構造器指向建構函式本身
function Star(name) {
this.name = name;
}
Star.prototype.dance = function () {
console.log(this.name);
};
let obj = new Star('小紅');
console.log(obj.__proto__); //{dance: ƒ, constructor: ƒ}
console.log(obj.__proto__.constructor); // Star
方法2:Star.prototype = {}
給原型重新賦值,此時會丟失構造器,我們需要手動定義構造器,指回建構函式本身
function Star(name) {
this.name = name;
}
Star.prototype = {
dance: function () {
console.log(this.name);
}
};
let obj = new Star('小紅');
console.log(obj.__proto__); //{dance: ƒ}
console.log(obj.__proto__.constructor); // ƒ Object() { [native code] }
Star.prototype.constructor = Star;
4.5.2 一般不允許直接改變原型prototype
指向
改變原型指向,會使原生的方法都沒了,所以Array、String這些內建的方法是不允許改變原型指向的。如果改變了,就會報錯。
Array.prototype.getSum = function (arr) {
let sum = 0;
for (let i = 0; i < this.length; ++i) {
sum += this[i];
}
return sum;
};
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(arr.getSum());//45
如果改變prototype指向,會報錯!
Array.prototype = {
getSum: function (arr) {
let sum = 0;
for (let i = 0; i < this.length; ++i) {
sum += this[i];
}
return sum;
}
};
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(arr.getSum());//45
五、繼承 - ES5方法
ES6之前並沒有給我們提供extends繼承,我們可以透過建構函式+原型物件模擬實現繼承。
繼承屬性,利用call改變this指向。但該方法只可以繼承屬性,例項不可以使用父類的方法。
function Father(name) {
this.name = name;
}
Father.prototype.dance = function () {
console.log('I am dancing');
};
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
let son = new Son('小紅', 100);
son.dance(); //報錯
如何繼承父類的方法呢?
解決方法1:利用Son.prototype = Father.prototype
改變原型指向,但此時我們給子類增加原型方法,同樣會影響到父類。
function Father(name) {
this.name = name;
}
Father.prototype.dance = function () {
console.log('I am dancing');
};
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
Son.prototype = Father.prototype;
//為子類新增方法
Son.prototype.sing = function () {
console.log('I am singing');
};
let son = new Son('小紅', 100);
//此時父類也被影響了
console.log(Father.prototype) //{dance: ƒ, sing: ƒ, constructor: ƒ}
解決方法2:子類的原型指向父類的例項,這樣就可以順著原型鏈共享父類的方法了。並且為子類新增原型方法的時候,不會影響父類。
function Father(name) {
this.name = name;
}
Father.prototype.dance = function () {
console.log('I am dancing');
};
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.sing = function () {
console.log('I am singing');
};
let son = new Son('小紅', 100);
console.log(Father.prototype) //{dance: ƒ, constructor: ƒ}
七、類
什麼是類?
類的本質還是一個函式,類就是建構函式的另一種寫法。
function Star(){}
console.log(typeof Star); //function
class Star {}
console.log(typeof Star); //function
ES6中類沒有變數提升
透過建構函式建立例項,是可以變數提升的。
es6中的類,必須先有類,才可以例項化。
類的所有方法都定義在類的prototype屬性上面
讓我們來測試一下。
class Father{
constructor(name){
this.name = name;
}
sing(){
return this.name;
}
}
let red = new Father('小紅');
let green = new Father('小綠');
console.log(red.sing === green.sing); //true
向類中新增方法
透過Object.assign,在原型上追加方法。
class Father{
constructor(name){
this.name = name;
}
sing(){
return this.name;
}
}
//在原型上追加方法
Object.assign(Father.prototype,{
dance(){
return '我愛跳舞';
}
});
let red = new Father('小紅');
let green = new Father('小綠');
console.log(red.dance());//我愛跳舞
console.log(red.dance === green.dance); //true
constructor方法
constructor方法是類的預設方法,透過new命令生成物件例項時,自動呼叫該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被預設新增。
八、繼承 - ES6方法
class Father {
constructor(name){
this.name = name;
}
dance(){
return '我在跳舞';
}
}
class Son extends Father{
constructor(name,score){
super(name);
this.score = score;
}
sing(){
return this.name +','+this.dance();
}
}
let obj = new Son('小紅',100);
九、類和建構函式的區別
(1) 類必須使用new呼叫,否則會報錯。這是它跟普通建構函式的一個主要區別,後者不用new也可以執行。
(2) 類的所有例項共享一個原型物件。
(3) 類的內部,預設就是嚴格模式,所以不需要使用use strict指定執行模式。
十、總結
建構函式特點:
1.建構函式有原型物件prototype。
2.建構函式原型物件prototype裡面有constructor,指向建構函式本身。
3.建構函式可以透過原型物件新增方法。
4.建構函式建立的例項物件有__proto__原型,指向建構函式的原型物件。
類:
1.class本質還是function
2.類的所有方法都定義在類的prototype屬性上
3.類建立的例項,裡面也有__proto__指向類的prototype原型物件
4.新的class寫法,只是讓物件原型的寫法更加清晰,更像物件導向程式設計的語法而已。
5.ES6的類其實就是語法糖。
十一、什麼是語法糖
什麼是語法糖?加糖後的程式碼功能與加糖前保持一致,糖在不改變其所在位置的語法結構的前提下,實現了執行時的等價。
語法糖沒有改變語言功能,但增加了程式設計師的可讀性。
十二、面試題分享
面試題1
Object.prototype.__proto__ //null
Function.prototype.__proto__ //Object.prototype
Object.__proto__ //Function.prototype
講解:
這裡涉及到Function的原型問題,附一張圖,這圖是一個面試官發給我的,我也不知道原作者在哪裡~
面試題2
給大家分享那道我被卡住的面試題,希望大家在學習完知識後,可以回顧一下。
按照如下要求實現Person 和 Student 物件
a)Student 繼承Person
b)Person 包含一個例項變數 name, 包含一個方法 printName
c)Student 包含一個例項變數 score, 包含一個例項方法printScore
d)所有Person和Student物件之間共享一個方法
es6類寫法
class Person {
constructor(name) {
this.name = name;
}
printName() {
console.log('This is printName');
}
commonMethods(){
console.log('我是共享方法');
}
}
class Student extends Person {
constructor(name, score) {
super(name);
this.score = score;
}
printScore() {
console.log('This is printScore');
}
}
let stu = new Student('小紅');
let person = new Person('小紫');
console.log(stu.commonMethods===person.commonMethods);//true
原生寫法
function Person (name){
this.name = name;
this.printName=function() {
console.log('This is printName');
};
}
Person.prototype.commonMethods=function(){
console.log('我是共享方法');
};
function Student(name, score) {
Person.call(this,name);
this.score = score;
this.printScore=function() {
console.log('This is printScore');
}
}
Student.prototype = new Person();
let person = new Person('小紫',80);
let stu = new Student('小紅',100);
console.log(stu.printName===person.printName);//false
console.log(stu.commonMethods===person.commonMethods);//true