寫多了
react
是否是已經忘記了什麼是原型鏈了,是否是已經忘記了那個純真騷年的那份初心了,前端,寫多了高大上的代碼,是否是應該靜下心來,好好學習下基礎,下面慢慢回溯下幾個常識點:前端
constructor
是每一個實例對象都會擁有的一個屬性,並且這個屬性的實在乎義在於一個指針,它指向了建立當前這個實例對象的類。react
function Person() {}
let p = new Person();
// ƒ Person() {}
console.log(p.constructor);
複製代碼
控制檯打印結果能夠看出,p.constructor
指向的是 Person
對象,後面會詳解 new
的過程。es6
constructor
的屬性值是能夠隨時改變的,若是不賦值,那就默認指向了建立這個實例對象的類,若是賦值了,那就會指向所賦值。api
在通常開發中,咱們是否是不多用到這個屬性啊,下面我就上點乾貨,來看看 Preact
源碼裏是怎麼使用這個屬性來解決業務場景的。數組
Preact
組件有兩種建立方式,一種是利用類建立,繼承 Preact.Component
父類或者不繼承,擁有這個父類的 render
方法等屬性,另外一種是經過 function
建立的無狀態組件(PFC),下面我就來講下 Preact
中是怎麼使用 constructor
屬性來處理的。babel
// 函數建立的無狀態組件
const Foo = () => {
return <div>Foo</div>;
};
// 常見的容器組件建立方式
class App extends Preact.Component {
render() {
return (
<div> <Foo /> </div>
);
}
}
複製代碼
// 上述組件通過babel後轉碼後的虛擬dom生成函數
Preact.createElement(
"div",
null,
React.createElement("p", null, "hahahaha"),
React.createElement(Foo, null)
);
// 該函數返回的是一個虛擬dom
var Foo = function Foo() {
return Preact.createElement("div", null, "Foo");
};
複製代碼
if(typeof type === 'function'){
...
}
複製代碼
上述代碼中,Preact.createElement
方法中的第一個參數就是 type
,其中 Foo
就是 function
類型。dom
Foo
函數的兩種形式if (Foo.prototype && Foo.prototype.render) {
}
複製代碼
在代碼中會判斷 Foo
函數是否能訪問 render
方法,首次渲染確定是沒有的,全部,上述的判斷會斷定 false
,關鍵點來了,下面來看看若是處理的:函數
首先來看下 Preact.Component
代碼的實現:oop
function Component(props, context) {
this.context = context;
this.props = props;
this.state = this.state || {};
// ...
}
Object.assign(Component.prototype, {
setState(state, callback) {},
forceUpdate(callback) {},
render() {}
});
複製代碼
能夠看出,若是是容器組件,繼承了父類 Preact.Component
,就可以訪問 render
方法,那麼若是是無狀態組件,怎樣讓這個組件擁有 render
方法:學習
let inst = new React.Component(props, context);
inst.constructor = Foo;
inst.render = function(props, state, context) {
return this.constructor(props, context);
};
複製代碼
起初看這個寥寥幾行代碼,包含了很多細緻的東西。
首先,它定義了 Preact.Component
這個類的實例對象 inst
,此時,這個 inst
的 constructor
默認指向 Preact.Component
這個類,接下來,給 inst
的 constructor
這個屬性賦值了,改變指向函數 Foo
,最後給這個實例對象 inst
添加一個 render
方法,核心就在這個方法,這個方法執行了 this.constructor
,其實就是執行了 Foo
方法,而 Foo
方法最終返回的就是一個虛擬 dom。
如今就說通了,其實,無狀態組件最終也會擁有一個 render
方法,觸發後會返回一個虛擬 dom 或者是子組件。
let inst = new React.Component(props, context);
inst.render = function(props, state, context) {
return Foo(props, context);
};
複製代碼
或許你能夠說徹底能夠不用 constructor
的也能實現啊,這就是 preact
的精妙之處了,在源碼中會有一個數組隊列 recyclerComponents
,這是專門用來回收銷燬組件的,它的判斷依據也是利用 constructor
屬性:
if (recyclerComponents[i].constructor === Foo) {
// ...
}
複製代碼
js 每一個對象都會擁有一個原型對象,即 prototype
屬性。
function Person() {}
複製代碼
Person
對象的原型對象就是 Person.prototype
對象:
Person.prototype
對象裏有那些屬性:
能夠看出這個對象默認擁有兩個原生屬性 constructor
和 __proto__
。
constructor
上面說過了,全部的對象都會有,那麼 __proto__
也是全部的對象都會有,它是一個內建屬性,經過它能夠訪問到對象內部的 [[Prototype]]
,它的值能夠是一個對象,也能夠是 null
。
那麼 __proto__
究竟是什麼呢:
function Person() {}
let p1 = new Person();
複製代碼
圖中的兩個紅框能夠看出,p1.__proto__
和 Person.prototype
指向了同一個對象。
// true
p1.__proto__ === Person.prototype;
複製代碼
Person
對象能夠從這個原型對象上繼承其方法和屬性,因此 Person
對象的實例也能訪問原型對象的屬性和方法,可是這些屬性和方法不會掛載在這個實例對象自己上,而是原型對象的構造器的原型 prototype
屬性上。
那麼,Person.prototype
的 __proto__
又指向哪裏呢?
看上圖,能夠看出 p1.__proto__.__proto__
指向了 Object.prototype
,p1.__proto__.__proto__.__proto__
最後指向了 null,由此能夠看出了構建了一條原型鏈。
原型鏈的構建依賴於實例對象的 __proto__
,並非原對象的 prototype
ES6 設置獲取原型的方法:
// 給p1原型設置屬性
Object.setPrototypeOf(p1, { name: "zhangsan" });
// zhangsan
console.log(p1.name);
// {name: "zhangsan"}
Object.getPrototypeOf(p1);
複製代碼
上圖紅框能夠看出,Object.setPrototypeOf
其實就是新的語法糖,至關於給 P1.__proto__
這個屬性賦值。
一個簡單的案例:
經典的原型鏈圖示:
舉例,如何用原生 js 實現 Student
繼承 Person
:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
function Student(name, age) {
this.name = name;
this.age = age;
}
Student.prototype.getInfo = function() {
return `${this.name} and ${this.age}`;
};
複製代碼
實現繼承,即要 Student
的實例可以訪問 Person
的屬性和方法,也要能訪問 Person
原型上的方法 getName
。
首先來看下 es6 的繼承:
class Person {
public name: string
constructor(name) {
this.name = name
}
getName(){
return this.name
}
}
class Student extends Person {
public age: number
constructor(name, age) {
super(name)
}
getAge() {
return this.age
}
}
let s = new Student('zhangsan', 20)
// zhangsan
s.name
// zhangsan
s.getName()
// 20
s.getAge()
複製代碼
那麼,用原生 js 怎麼作呢,下面來一步一步的實現。
call
實現函數上下文繼承function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
function Student(name, age) {
Person.call(this, name);
this.age = age;
}
Student.prototype.getInfo = function() {
return `${this.name} and ${this.age}`;
};
let s = Student("zhangsan", 20);
// zhangsan
s.name;
// error
s.getName();
複製代碼
call
方法只是改變了 Person
中函數體內的 this
指向,並不能改變它的原型,因此沒法訪問 Person
方法的原型。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
function Student(age) {
this.age = age;
}
Student.prototype.getInfo = function() {
return this.age;
};
Student.prototype = new Person("zhangsan");
let s = new Student(20);
// zhangsan
s.getName();
複製代碼
Student.prototype
的值設置爲父類的實例對象,這樣就能很簡單的實現 Student
的實例對象能訪問到 Person
的原型,可是這也是也有問題的,與其說繼承的是 Person
這個類,不如說是繼承的是這個類的實例對象,就是 name = zhangsan
這個實例,和 oop
的思想有背。
/** * 繼承函數的核心方法 */
function _extends(child, parent) {
// 定義一箇中間函數,並設置它的 constructor
function __() {
this.constructor = child;
}
// 這個函數的原型指向父類的原型
__.prototype = parent.prototype;
// 子類的原型窒息那個這個中間函數的實例對象
child.prototype = new __();
}
複製代碼
這個 _extends
方法,是實現的核心,兩個知識點,一是定義了一個無參數的中間函數,並設置它的 constructor
;第二個就是對原型鏈的使用。
function Person(name) {
this.name = name;
this.getName1 = function() {
console.log("Person", this.name);
};
}
Person.prototype.getName = function() {
console.log("Person prototype", this.name);
};
// 這個方法必定要在定義子類原型以前調用
_extends(Student, Person);
function Student(name, age) {
this.age = age;
Person.call(this, name);
}
Student.prototype.getInfo = function() {
console.log("Student", this.age);
};
let s = new Student("zhangsan", 12);
// Person prototype zhangsan
s.getName();
// Student 12
s.getInfo();
複製代碼
這樣,就能簡單是實現了繼承,而且多重繼承也是支持的
// 多重繼承
_extends(MidStudent, Student);
function MidStudent(name, age, className) {
this.className = className;
Student.call(this, name, age);
}
let mids = new MidStudent("lisi", 16, "class1");
// Person prototype lisi
mids.getName();
// Student 16
mids.getInfo();
複製代碼
有興趣能夠多多研究研究,網上有很多精品案例,這個是 js 的基礎,確定比成天調用 api 有意思的多,收穫的也會更多。
俗話說,沒有女友的你能夠new
一個對象,那麼這個 new
一下,到底經歷了什麼呢。
let p1 = new Person();
複製代碼
step1,讓變量
p1
指向一個空對象
let p1 = {};
複製代碼
step2, 讓
p1
這個對象的__proto__
屬性指向Person
對象的原型對象
p1.__proto__ = Person.prototype;
複製代碼
step3, 讓
p1
來執行Person
方法
Person.call(p1);
複製代碼
如今看這個流程,是否是很簡單,是否是有種豁然開朗的感受!
那要如何實現一個本身的 new
呢?
/** * Con 目標對象 * args 參數 */
function myNew(Con, ...args) {
// 建立一個空的對象
let obj = {};
// 連接到原型,obj 能夠訪問到構造函數原型中的屬性
obj.__proto__ = Con.prototype;
// 綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
let ret = Con.call(obj, ...args);
// 優先返回構造函數返回的對象
return ret instanceof Object ? ret : obj;
}
複製代碼
來測試下:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
console.log(`your name is ${this.name}`);
};
let p2 = myNew(Person, "lisi");
// your name is lisi
p2.getName();
複製代碼
完美實現了!