細說javascript的constructor和prototype

寫多了 react 是否是已經忘記了什麼是原型鏈了,是否是已經忘記了那個純真騷年的那份初心了,前端,寫多了高大上的代碼,是否是應該靜下心來,好好學習下基礎,下面慢慢回溯下幾個常識點:前端

  • constructor 構造器
  • prototype 原型鏈
  • new 一個對象到底發生了什麼

constructor 構造器

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 轉碼
// 上述組件通過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");
};
複製代碼
  • 虛擬 dom 中的類型判斷
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,此時,這個 instconstructor 默認指向 Preact.Component 這個類,接下來,給 instconstructor 這個屬性賦值了,改變指向函數 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) {
  // ...
}
複製代碼

prototype 原型鏈

js 每一個對象都會擁有一個原型對象,即 prototype屬性。

function Person() {}
複製代碼

Person 對象的原型對象就是 Person.prototype 對象:

原型

Person.prototype 對象裏有那些屬性:

原型屬性

能夠看出這個對象默認擁有兩個原生屬性 constructor__proto__

constructor 上面說過了,全部的對象都會有,那麼 __proto__ 也是全部的對象都會有,它是一個內建屬性,經過它能夠訪問到對象內部的 [[Prototype]] ,它的值能夠是一個對象,也能夠是 null

那麼 __proto__ 究竟是什麼呢:

function Person() {}
let p1 = new Person();
複製代碼

__proto__

圖中的兩個紅框能夠看出,p1.__proto__Person.prototype 指向了同一個對象。

// true
p1.__proto__ === Person.prototype;
複製代碼

三者關係

Person 對象能夠從這個原型對象上繼承其方法和屬性,因此 Person 對象的實例也能訪問原型對象的屬性和方法,可是這些屬性和方法不會掛載在這個實例對象自己上,而是原型對象的構造器的原型 prototype 屬性上。

那麼,Person.prototype__proto__ 又指向哪裏呢?

__proto__

看上圖,能夠看出 p1.__proto__.__proto__ 指向了 Object.prototypep1.__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

上圖紅框能夠看出,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 一個對象,那麼這個 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();
複製代碼

完美實現了!

相關文章
相關標籤/搜索