ES6之那些年咱們都迷茫的原型和類(上)

ES5之原型

本文會分爲上下兩篇。上篇會講 ES5 相關的東西, 下篇會講 ES6 相關的東西。前端

函數聲明 和 函數表達式

既然是講類,那不得不從函數開始講起,以及涉及到到的原型鏈相關的東西。咱們寫第一個 hello world 時的場景不知道大家還記不記得。閉包

function HelloWorld() {
      console.log('hello world')
  }
複製代碼
var HelloWorld = function() {
    console.log('hello world')
}
複製代碼

函數聲明 與 函數表達式 的最主要區別就是函數名稱, 在函數表達式只可以能夠忽略它,從而建立匿名函數,一個匿名函數能夠被用做一個IIFE(即時調用的函數表達式), 一旦它定義就運行。 注意點: 函數表達式沒有提高,不像函數聲明, 你在定義函數表達式以前不能使用函數表達式app

obj();

const obj = function() {
    console.log('obj')
}

//ReferenceError: Cannot access 'obj' before initialization
複製代碼

若是想再函數體內部引用當前函數, 則須要建立一個命名函數表達式。 而後函數名稱將會做爲函數體(做用於)的本地變量函數

var math = {
    'factorial': function factorial(n) {
        if (n <= 1) {
            return 1
        }
        return n * factorial(n - 1)
    }
}
const obj = math.factorial(3);
console.log('obj: ', obj); //6
複製代碼

被函數表達式賦值的那個變量會有一個 name 屬性, 若是咱們直接調用這個跟函數名有區別嗎?學習

對象方法、 類方法、 原型方法

function People(name) {
    this.name = name;
    //對象方法 
    this.Introduce = function() {
        alert("My name is " + this.name);
    }
}
//類方法 
People.Run = function() {
    alert("I can run");
}
//原型方法 
People.prototype.IntroduceChinese = function() {
    alert("個人名字是" + this.name);
}

//測試 

var p1 = new People("Windking");

p1.Introduce(); //對象方法須要經過實例化對象去調用 

People.Run(); //類方法不須要經過實例化對象去調用 

p1.IntroduceChinese(); //原型方法也須要經過實例化對象去調用
複製代碼
  1. 對象方法包括構造函數中的方法以及構造函數原型上面的方法。
  2. 類方法,至關於函數, 能夠爲其添加函數屬性及方法。
  3. 原型方法通常用於對象實例共享,好比 Person.prototype.sayName=function(){console.log(this.name); }; 在原型上面添加該方法,就能實現共享。這樣就不用每一次初始化一個實例的時候,爲其分配相應的內存了。

原型鏈

每個切圖仔最開始接觸前端,最頭疼的就是原型與原型鏈相關的東西。那麼咱們先來梳理下。測試

原型是什麼?ui

在JavaScript中原型是一個prototype對象,用於表示類型之間的關係。this

原型鏈是什麼?spa

JavaScript 萬物都是對象,對象和對象之間也有關係,並非孤立存在的。對象之間的繼承關係,在 JavaScript 中是經過 prototype 對象指向父類對象,直到指向 Object 對象爲止,這樣就造成了一個原型指向的鏈條,專業術語稱之爲原型鏈。prototype

在咱們學習以前先來看幾個問題理清幾個概念

prototype 和 proto

其實在 JavaScript 代碼尚未運行的時候,JavaScript 環境已經有一個 window 對象 window 對象有一個 Object 屬性, window. Object 是一個函數對象. window. Object 這個函數對象有一個重要的屬性是 prototype.

var obj = {};
obj.toString();
console.log('obj', obj)
複製代碼

obj.prototype

obj變量指向一個空對象,這個空對象有個__proto_ 屬性指向 window. Object.prototype 調用obj.toString()的時候,obj自己沒有toString,就去obj._proto_上面去找toString 因此你調用obj.toString的時候,其實是調用window. Object.prototype.toString

咱們在看這個例子:

var arr = [];
arr.push(1)
console.log(arr)
複製代碼

arry.__proto_

arr.proto 指向window. Array.prototype。 這樣當你在調用 arr.push 的時候,arr 自身沒有 push 屬性,就上去arr.proto 上找。最終找到 push 方法。若是是 arr.valueOf 呢,arr 自身沒有,Array.prototype 也沒有, 那麼他會去 arr.proto.proto 就是 window. Object.prototype, 因此 arr.valueOf 其實就是 window. Object.valueOf

因此咱們能夠得出以下概念:

prototype 是構造函數的屬性,構造函數也是對象。 而 proto 是對象的屬性, 函數的 prortotype 是個一對象, 對象的 proto 屬性指向原型 , proto 將對象和原型鏈接起來組成了原型鏈。

  • Object 是全部對象的爸爸, 全部對象均可 proto 指向
  • Funciton 是全部函數的爸爸, 全部函數均可以經過 proto 找到它

他們的區別:

  • prototype 是讓你知道用什麼屬性
  • proto 是讓你知道都有什麼屬性

constructor 和 prototype

原型(prototype)是構造函數的一個屬性,是一個對象。constructor 是綁在實例上面的,不是綁在原型鏈上面的。,constructor 則表明實例擁有的方法。能夠淺顯的認爲 prototype 向下指, constructor 向上指, 這裏的向上指表明的是往父類或者原型上面。

構造函數

var obj = new Object();
console.log('Object.prototype.constructor == Objcect && Objcect === obj.constructor', Object.prototype.constructor == Object && Object == obj.constructor)
//這個答案是什麼
複製代碼

在前面說過,prototype 是讓你知道用什麼屬性,Object.prototype 指的是 Object類原型的 constructor方法。

function Bottle() {
        this.name = 'a';
        this.sayHello = function() {
            console.log('this.name', this.name);
        }
    }
    Bottle.prototype.sayIntroduce = function() {
        console.log('this.sayIntroduce', this.name);
    }
    var bot1 = new Bottle();
    var bot2 = new Bottle();
    console.log(Bottle.prototype.constructor == Bottle); //ture
    console.log(bot1);
複製代碼

原型鏈

構造函數實例出來的對象,能夠獲得構造函數對象中的屬性,方法。等等還有一個什麼 proto。咱們仔細點進去,有兩個東西 constructor: Bottle()。這是由於咱們是由 Bottle,new出來。咱們在繼續點下去,還有__proto_: 的constructor: Object()。

var obj = new Object();
obj.__proto = 六個屬性:constructor, hasOwnProperty, toLocaleString

obj.constructor指向Objector

複製代碼
function Person(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

Person.prototype.sayHello = function() {
    console.log(this.name)
}
複製代碼

這是咱們構造函數的方法, 咱們添加下面東西 看能輸出什麼

var obj1 = new Person('red', 10, 'man');
var obj2 = new Person('yellow', 11, 'male');
console.log('obj1.sayHello === obj2.sayHello', obj1.sayHello === obj2.sayHello)
複製代碼

經過構造函數生成的實例對象時,會自動爲實例對象分配原型對象。每一個構造函數都有一個 prototype 屬性,這個屬性就是實例對象的原型對象。

原型對象上的全部屬性和方法, 都能被派生對象共享。

function Person(name) {}

Person.prototype = {
    constructor: Person,
    sayHello: function() {

    }
}
複製代碼

注意:需注意的是在上面的代碼中,咱們將 Person.prototype 設置爲一個新建立的對象。會致使 Person.prototype 對象原來的 constructor 屬性再也不指向 Person, 這裏能夠像上面那樣,特地的把 constructor 設置爲 Person 。

var arr = [];
arr.name = "ar1";
Array.prototype.name = "Ar1";
console.log(arr.name); // ar1
複製代碼

當構造函數自定義的屬性名與該構造函數下原型屬性名相同時,構造函數的自定義屬性優先於原型屬性(能夠把構造函數理解爲內聯樣式), 而原型屬性或者原型方法能夠看作是class)

var obj = {}; //建立空對象
obj.__proto__ = MyFunc.prototype; //將這個空對象的__proto__成員指向了構造函數對象的prototype成員對象

MyFunc.call(obj); //將構造函數的做用域賦給新對象

return obj //返回新對象obj
複製代碼

因此在這裏簡單總結下構造函數、原型、隱式原型和實例的關係:每一個構造函數都有一個原型屬性(prototype),該屬性指向構造函數的原型對象;而實例對象有一個隱式原型屬性(proto),其指向構造函數的原型對象(obj.proto==Object.prototype);同時實例對象的原型對象中有一個constructor屬性,其指向構造函數。

因爲prototype是經過函數名,指到其餘內存空間獨立的函數體,所以無法取得閉包的做用域變量。

Person.prototype.myAge = function() {
    console.log(age);
};
var p1 = new Person(20);// 新建對象p1
p1.myAge();// 報錯 age is not defined
複製代碼

繼承 多態 封裝

子類繼承父類

  1. 借用構造函數繼承

call/apply 將 子類 的 this 傳給 父類 , 再將 父類的屬性綁定到 子類 的 this 上。

function Person(name, grade) {
    this.name = name //實例屬性
    this.grade = grade // 實例屬性
    this.sayHello = function() { //實例方法
        console.log('hello', this.name, this.grade)
    }
}

function Student(name, grade, sex) {
    Person.apply(this, arguments)
    // 或者 Person.call(this, name, grade)
    //父類 沒有構造的屬性 沒法直接用
    this.sex = sex;
    this.sayIntroduce = function() {
        console.log('sayIntroduce', this.name, this.grade, this.sex)
    }
}
var std1 = new Student('b', 11, 'man')
var Per1 = new Person('a', 23, 'man')
std1.sayIntroduce();
std1.sayHello();

console.log('Per1.name', Per1.sex) //拿不到子類的東西, 否則就是雙向綁定了
Per1.sayIntroduce() //拿不到子類的東西, 否則就是雙向綁定了

console.log('std1.name: ', std1.name);
console.log('std1.grade: ', std1.grade);
console.log('std1.sex: ', std1.sex);

console.log(" student instanceof Student", std1 instanceof Student)
console.log(" student instanceof Student", std1 instanceof Person)
複製代碼

instanceof 運算符能夠用來判斷某個構造函數的 prototype 屬性是否存在另一個要檢測對象的原型鏈上.

這種在構造函數內部借用函數而不借助原型繼承的方式被稱之爲 借用構造函數式繼承. 可是這樣作的缺點就是沒有繼承 Person 的原型方法和屬性。

//咱們將上面 Person 類裏面的 this.sayHello 註釋掉
//補上
Person.prototype.sayHello = function() {
    console.log('hello', this.name)
}

std1.sayHello();
複製代碼

以上代碼有個弊端會報這個錯誤:ES5.JS:103 Uncaught TypeError: std1.sayHello is not a function

  1. prototype 模式

先看下面代碼

function Person(name) {
    this.name = name
    // this.sayHello = function() {
    // console.log('hello', this.name)
    // }
}

Person.prototype.sayHello = function() {
    console.log('hello', this.name)
};

function Student(name, grade, sex) {
    this.name = name;
    this.grade = grade
    this.sex = sex;
}

Student.prototype = new Person(); //經過改變原型對象實現繼承
Student.prototype.constrctor = Student; // 保持構造函數和原型對象的完整性
var std1 = new Student('b', 11)
var std2 = new Student('a', 22)
std1.sayHello(); // 實例和原型 均訪問的到。
console.log(std1.hasOwnProperty('name')) // 爲false 說明是繼承來的屬性, 爲 true 表明是自身的屬性
console.log(std1.sayHello === std2.sayHello) // true,複用了方法
console.log('std1.prototype: ', Student.prototype);
複製代碼

將子類的 prototype 指向父類的實例。 每一個 prototype 都有一個 constructor 屬性,它指向構造函數。 缺點就是子類實例沒有本身的屬性.

3.直接繼承 prototype

function Person(name) {
    this.name = name
    // this.sayHello = function() {
    // console.log('hello', this.name)
    // }
}

Person.prototype.sayHello = function() {
    console.log('hello', this.name)
};

function Student(name, grade, sex) {
    this.name = name;
    this.grade = grade
    this.sex = sex;
}

Student.prototype = Person.prototype;
Student.prototype.constrctor = Student;
var std1 = new Student('b', 11)
std1.sayHello(); // 實例和原型 均訪問的到。
console.log('std1.prototype: ', Student.prototype);
console.log(Person.prototype.constructor); //Student
複製代碼

缺點是 Student.prototype 和 Person.prototype 如今都指向同一個對象了,那麼任何對Student.prototype 修改, 都會映射到 Person.prototype 上。

  1. 空對象
var Obj = function() {}
Obj.prototype = Person.prototype
Student.prototype = Obj.prototype;
Student.prototype = new Obj();
Student.prototype.constructor = Student;
複製代碼

以上繼承方式或多或少都有點缺點,那麼咱們有沒有完美的解決方案呢

  1. 最佳組合方式
function Animal(name) {
    this.name = name
}
Animal.prototype.species = 'animal'

function Leo(name) {
    Animal.call(this, name)
}
Leo.prototype = new Animal()
Leo.prototype.contructor = Leo

//既然不能直接在二者之間畫等號, 就造一個過渡紐帶唄. 可以關聯起原型鏈的不僅有 new,  Object.create() 也是能夠的.

function Animal(name) {
    this.name = name
}
Animal.prototype.species = 'animal'

function Leo(name) {
    Animal.call(this, name)
}
Leo.prototype = Object.create(Animal.prototype)
Leo.prototype.contructor = Leo

// 這種在構造函數內部借函數同時又間接藉助原型繼承的方式被稱之爲 寄生組合式繼承.
複製代碼

ES5方式的實現方式(最佳實踐)

最佳實踐其實就是在組合繼承的基礎上修改原型繼承的方式,封裝inheritPrototype函數,專門處理子類繼承父類的原型邏輯.inheritPrototype函數。

function Person(name) {
    this.name = name
}
Person.prototype.sayHello = function() {
    console.log(‘hi’ + this.name)
}

function Student(name, grade) {
    Person.call(this, name)
    this.grade = grade;

}

inheritPrototype(Student, Person);

Student.prototype.selfIntroduce = function() {
    console.log('my ' + this.name + ' grade ' + this.grade)
}

function inheritPrototype(subType, superType) {
    var protoType = Object.create(superType.prototype);
    protoType.constructor = subType;
    subType.prototype = protoType

}

var student = new Student('obj', 23)
console.log(student.name); //‘obj’
student.sayHello(); // obj
student.sayHello(); //23
student.hasOwnProperty(‘name’); //true
複製代碼

多態

JavaScript 的多態,咱們先看百度百科的介紹:多態(Polymorphism)按字面的意思就是「多種狀態」。 在面嚮對象語言中,接口的多種不一樣的實現方式即爲多態。 多態的優勢

  1. 擴展性強
  2. 消除類型之間的耦合關係
  3. 接口性
  4. 可替換行

存在的三個必要條件

  • 繼承
  • 重寫
  • 父類引用指向子類對象
function Person(){
    this.say = function(vocation){
      console.log("My vocation is" , vocation.name)
      console.log("My vocation is" , vocation.constructor)
    }
  }

  
  
  function Student(){
    this.name = name
  }
  
  function Teacher(name){
    this.name = name
  }
  var std = new Student('student')
  var tea = new Teacher('teacher')
  var per = new Person();
  per.say(std);
  per.say(tea);



複製代碼

封裝

封裝的表現形式,所解決的問題是,私有變量,不讓外部訪問到。

function Person(name, age) {
    this.name = name;
    var age = age;// 在實例中沒法被調用
}
var p1 = new Person("obj`", 20);
console.log(p1) // Person ->{name: "obj"} 沒法訪問到age屬性,這就叫被封(裝)起來了。
複製代碼

咱們能夠用特權方法訪問到私有變量

function Person(age) {
    var age = age;// 私有變量
    this.showAge = function() {// 特權方法
        console.log(age);
    };
}
var p1 = new Person(20);// 新建對象p1
p1.showAge();
複製代碼
相關文章
相關標籤/搜索