夯實基礎-數據類型與繼承

數據類型是基礎中的基礎,你們每天遇到,咱們此次來討論深一點,將咱們認爲理所固然的事情背後的原理髮掘;繼承也是前端基礎一個大考點,看看繼承的原理與使用場景。javascript

本文討論如下幾個點:html

  1. JavaScript數據類型
  2. 不一樣數據類型對應的數據結構
  3. 數據類型轉換
  4. 數組與對象的api表
  5. new關鍵字背後幹了些什麼
  6. 原型與原型屬性與構造函數區別於聯繫
  7. 實例化,混入,繼承,多態是什麼意思

數據類型

最新的 ECMAScript 標準定義了 7 種數據類型:前端

這個分類咱們應該是至關熟悉了,當時這是按照什麼標準分類的。git

數據類型對應的數據結構

事實上上面的分類標準是按照不一樣數據在計算機內存中的結構分類的。咱們都知道JavaScript中的變量運行的時候是存在內存中的,若是接觸過java的人應該知道,內存中也分爲棧內存和堆內存。github

棧(stack)

基本類型Undefined、Null、Boolean、Number 和String。這些類型在內存中分別佔有固定大小的空間,他們的值保存在棧內存,他們的值保存在棧內存,咱們經過按值來訪問的。面試

var a = '1';
var b = '1';
a === b;

上述代碼執行時候,能夠理解爲:編程

  1. 聲明變量a,b,爲a,b分配一個棧內存空間。(變量提高)
  2. 要賦值a,將a的字面量'1'做爲值存儲到a在棧內存的值中。
  3. 要賦值b,一樣將'1'做爲棧內存的值存儲。
  4. 這種簡單數據類型,都是在棧內存中保存其值。

JavaScript中的原始值(基本數據類型)均可預知其最大最小內存大小,因此建立的時候直接分配對應的內存空間。segmentfault

堆(heap)

複雜的數據類型,如object,array,function等,沒法提早預知其要佔用多少內存空間,因此這個數據類型被放入了堆內存中,同時在棧內存中保存其堆內存的地址,訪問這些變量的時候,在棧內存中獲取到其內存地址,而後訪問到該對象,這種方式叫按引用訪問api

var a = 'hello world';
var b = 123;
var c = null;
var d = undefined;
var e = {};
var f = function(){console.log(1);};
var g = [1,2,a];

其在內存中的簡易模型以下:

clipboard.png

上面這個圖並非徹底準確的,這裏只是簡單形容一下不一樣數據類型變量的存儲關係,偏底層的知識真的須要單獨開一篇來說了。

經過上面的圖我想應該一目瞭然了,基本數據類型都是存在棧內存中的,複雜對象則是存在堆內存中,棧內存變量保存的是其內存地址。這也應該想到了咱們常常遇到的問題:對象之間賦值,賦值的是真正的內存地址;對象相互比較===,比較的是內存地址。

變量賦值

JavaScript 引用指向的是值。若是一個值有 10 個引用,這些引用指向的都是同一個值,它們相互之間沒有引用 / 指向關係。

JavaScript 對值和引用的賦值 / 傳遞在語法上沒有區別,徹底根據值的類型來決定:

  1. 簡單數據類型老是經過值複製的方式來賦值 / 傳遞。
  2. 複雜數據類型則老是經過引用複製的方式來賦值 / 傳遞。

包裝類和類型轉換

內置對象

JavaScript內置了一些對象,這些對象能夠在全局任意地方調用,而且有各自的屬性和方法。MDN上羅列了所有,這裏只挑一部分對象說明:

  • Object
  • Array
  • Function
  • String
  • Number
  • Boolean
  • Math
  • Date
  • RegExp

ok經過上面的幾個內置對象就會發現一些問題:一些基本數據類型(String,Number,Boolean)有對應的內置對象,可是其餘的一些(Null, Undefined)就沒有,複雜數據類型則都有,這是爲何。

包裝類

var a = 'hello world';
a[1]; // 'e'
a.length; // 11
a.toString(); // hello world
a.valueOf(); // hello world
a.split(' '); // ['hello', 'world']

有沒有想過,變量a命名是個基本類型,不是對象,爲何會有這麼多屬性和方法。由於這些內置的屬性和方法都在內置對象String上。

事實上當你調用這些基本數據類型上屬性和方法時候,引擎會自動尋找其是否有對應的包裝類,有的話生成一個包裝類的實例供你使用(使用以後銷燬),不然報錯。

var a = 'hello world';
a.customAttribute // undefined
String.prototype.customAttribute = 'custom';
var b = 'hello world';
b.customAttribute // custom

咱們如今想要訪問屬性customAttribute,這個屬性沒有在內置對象上,因此獲取到的值是undefined;咱們向內置對象的原型鏈上添加該屬性,以後全部的string上均可以獲取到該值。

類型轉換

JavaScript中的類型轉換也是個大坑,很多面試都會問到。JavaScript 是一種動態類型語言,變量沒有類型限制,能夠隨時賦予任意值。

顯示轉換

直接調用對應的包裝類進行轉換。具體可分紅三種狀況:

// 數值:轉換後仍是原來的值
Number(324) // 324

// 字符串:若是能夠被解析爲數值,則轉換爲相應的數值
Number('324') // 324

// 字符串:若是不能夠被解析爲數值,返回 NaN
Number('324abc') // NaN

// 空字符串轉爲0
Number('') // 0

// 布爾值:true 轉成 1,false 轉成 0
Number(true) // 1
Number(false) // 0

// undefined:轉成 NaN
Number(undefined) // NaN

// null:轉成0
Number(null) // 0

使用Number包裝類來進行類型轉換,隱藏的邏輯:

  1. 調用對象自身的valueOf方法。若是返回原始類型的值,則直接對該值使用Number函數,再也不進行後續步驟。
  2. 若是valueOf方法返回的仍是對象,則改成調用對象自身的toString方法。若是toString方法返回原始類型的值,則對該值使用Number函數,再也不進行後續步驟。
  3. 若是toString方法返回的是對象,就報錯。
var obj = {x: 1};
Number(obj) // NaN

// 等同於
if (typeof obj.valueOf() === 'object') {
  Number(obj.toString());
} else {
  Number(obj.valueOf());
}

var obj1 = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return {};
  }
};

Number(obj1)
// TypeError: Cannot convert object to primitive value

Number({
  valueOf: function () {
    return 2;
  }
})
// 2

Number({
  toString: function () {
    return 3;
  }
})
// 3

Number({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
})
// 2

若是使用String則規則相對簡單:

  1. 值爲基本數據類型

    • 數值:轉爲相應的字符串。
    • 字符串:轉換後仍是原來的值。
    • 布爾值true轉爲字符串"true"false轉爲字符串"false"
    • undefined:轉爲字符串"undefined"
    • null:轉爲字符串"null"
  2. 值爲對象

    1. 先調用對象自身的toString方法。若是返回原始類型的值,則對該值使用String函數,再也不進行如下步驟。
    2. 若是toString方法返回的是對象,再調用原對象的valueOf方法。若是valueOf方法返回原始類型的值,則對該值使用String函數,再也不進行如下步驟。
    3. 若是valueOf方法返回的是對象,就報錯。

Boolean規則更簡單:除了五個值(undefined,null,(+/-)0,NaN,‘’)的轉換結果爲false,其餘的值所有爲true

隱式轉換

隱式轉換也分三種狀況:

轉布爾值

JavaScript 遇到預期爲布爾值的地方(好比 if語句的條件部分),就會將非布爾值的參數自動轉換爲布值。系統內部會自動調用 Boolean函數。

因此跟上面同樣,所以除了五個值(undefined,null,(+/-)0,NaN,‘’),其餘都是自動轉爲true

轉字符串

JavaScript 遇到預期爲字符串的地方,就會將非字符串的值自動轉爲字符串。具體規則是,先將複合類型的值轉爲原始類型的值,再將原始類型的值轉爲字符串。

字符串的自動轉換,主要發生在字符串的加法運算時。當一個值爲字符串,另外一個值爲非字符串,則後者轉爲字符串。

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

轉數值

JavaScript 遇到預期爲數值的地方,就會將參數值自動轉換爲數值。系統內部會自動調用 Number函數。

除了加法運算符(+)有可能把運算子轉爲字符串,其餘運算符都會把運算子自動轉成數值。

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

具體參考阮一峯老師:JavaScript類型轉換

數組和對象

這三個複雜對象咱們太熟悉不過了,天天都在打交道。可是實際上咱們也並非徹底掌握。

數組(Array)

數組方法不少,咱們能夠分類來整理記憶。

有哪些方法返回的是新數組

  1. concat
  2. slice
  3. filter
  4. map
  5. forEach

遍歷數組方法有幾種,區別在於什麼

常見的有:

  1. map:返回新數組,數組的每一項都是測試函數的返回值。
  2. forEach:不返回任何值,只是單純遍歷一遍數組。
  3. every:遍歷數組全部元素,直到測試函數返回第一個false中止。
  4. some:遍歷數組全部元素,直到測試函數返回第一個true中止。
  5. for循環:寫起來最麻煩,可是性能最好。

filter方法傳入函數的參數有幾個,都是什麼含義

不僅是filter方法,相似這種第一個參數爲callback的方法如:some,every,forEach,map,find,findIndex的方法callback參數都同樣:currentValue,Index,array。
github上參照了MDN整理了一份完整的文檔,用於本身的查缺補漏。

對象(Object)

建立對象的方法有幾種

  1. 字面量方式:

    var person={
        name:"SF",
        age:25
        say:function(){
           alert(this.name+"今年"+this.age);
        }
    };
    person.say();
  2. 利用Object對象建立實例

    var my = new Object();
    my.name = "SF"; //JavaScript的發明者
    my.age = 25;
    my.say = function() { 
      alert("我是"+this.name+"今年"+my.age);
    }
    my.say();
    
    var obj = Object.create(null);
    obj.name = 'SF';
  3. 構造函數

    function Person(name,age) { 
      this.name = name; 
      this.age = age; 
      this.say = function() { 
          alert("我叫" + this.name + ",今年" + this.age + "歲); 
      }
    }
    var my = new Person("SF",25); //實例化、建立對象
    my.say(); //調用say()方法
  4. 原型模式

    function Person() {
    }
    Person.prototype.name = 'aus';
    Person.prototype.job = 'fe'
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    var person1 = new Person();
  5. 組合構造函數和原型

    function Person( name, age, job ) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.friends = ["Shelby","Court"];
    }
    
    Person.prototype = {
        constructor: Person,
        sayName: function(){
            alert(this.name);
        }
    }
    
    var person1 = new Person("Nicholas", 29, "software Engineer");
    var person2 = new Person("Greg", 27, "Doctor");
    
    person1.friends.push("Van");
    
    alert(person1.friends); //"Shelby,Court,Van"
    alert(person2.friends); //"Shelby,Court"
    alert(person1.friends === person2.friends); //false
    alert(person1.sayName === person2.sayName); //true

對象的擴展密封和凍結有什麼區別

  • 擴展特性

    • Object.isExtensible 方法
    • Object.preventExtensions 方法
  • 密封特性

    • Object.isSealed 方法
    • Object.seal 方法
  • 凍結特性

    • Object.isFrozen 方法
    • Object.freeze 方法

      • 淺凍結深凍結

簡單說就是對象有可擴展性(能夠隨意添加屬性),限制對象的可擴展性(Object.preventExtensions)以後,對象不可添加新屬性(可是現有屬性能夠修改和刪除)。

密封對象(seal)指的是對象的屬性不可增長或者刪除,而且屬性配置不可修改(屬性值可修改)。

凍結對象(freeze)則更加嚴格,不可增長或者刪除屬性,而且屬性徹底不可修改。

這裏不作過多介紹,詳細能夠看這裏

怎樣快速實現淺拷貝以及深拷貝

Object.assign是常見的淺拷貝方法,怎樣本身實現。

// 利用原生api
function shallowClone(obj) {
  return Object.create(
      Object.getPrototypeOf(obj), 
      Object.getOwnPropertyDescriptors(obj) 
  );
}

// 屬性淺拷貝
function shallowCopy(copyObj) {
  var obj = {};
  for ( var i in copyObj) {
    obj[i] = copyObj[i];
  }
  return obj;
}

深拷貝以前整理過:github

對象的方法參照MDN整理了一份,github

原型鏈

這節算是給繼承鋪墊基礎知識了,js裏最出名的原型和原型鏈,面試必考,平常開發也特別常見。

prototype

prototype中文譯爲'原型',大部分Object和Function都有prototype。我的以爲原型是一個特殊的普通對象,對象裏面的屬性和方法都用於指定的用途:共享。咱們能夠按照本身的意願去修改原型,而且從新被共享。

當建立函數的時候,每個函數都會自動有一個prototype屬性,這個屬性的值是空對象(空對象不是空)。

一旦你把這個函數當成構造函數調用(經過new調用)JS會建立構造函數的實例,實例是不具備原型的。

function A (){};
A.prototype // {}

var a = new A();
a.prototype // undefined

proto

中文翻譯過來叫'原型屬性',這是一個隱式屬性,不可被枚舉,可是他的用途相當重要。每一個對象建立的時候,都會有一個隱式的屬性__proto__,該屬性的值是其對應的原型(其實就是說明 該對象的來源)。

function A (){};
A.__proto__ === Function.prototype; // true

var b = {};
b.__proto__ === Object.prototype; // true

var c = [];
c.__proto__ === Array.prototype; // true

能夠肯定的是,__proto__指向的是其構造函數的原型

contructor

構造函數實例都擁有指向其構造函數的constructor屬性。constructor屬性的值是一個函數對象 爲了將實例的構造器的原型對象暴露出來。

function A(){};
A.constructor === Function // true

var a = new A();
a.construtor === A // true

var obj = {};
obj.constructor === Object // true

能夠肯定的是,constructor屬性指向其構造函數

關係

上面三者的關係能夠用下圖表示:
clipboard.png
這裏就不得不提一句:使用new關鍵字實例化對象,內在過程到底發生了什麼。

咱們能夠理解爲將new關鍵字實例化對象拆成兩步:

function A(){};

function create (base) {
    var obj  = {};
    obj.__proto__ = base.prototype;
    base.call(obj);
    return obj;
}

var a = create(A);

a instanceof A // true

原型鏈

上面三個角色到期了以後,就到了另外一個重點:原型鏈。

var a = Object.create(null);
a.a = 1;
var b = Object.create(a);
b.b = 2;
var c = Object.create(b);
c.c = 3;

c.a // 1
c.b // 2
c.c // 3

a.d = 4;
c.d;

c.a = 0;
c.a; // 0

上面這個例子用到了Object.create函數建立了一個原型爲空的對象a。能夠看到c並無a,b屬性,可是卻能夠讀出該值來,這就是原型鏈。

當訪問一個對象的屬性(方法)的時候,若是對象自身沒有該屬性(方法),就會去該對象的__proto__上尋找,若是__proto__上也沒有,就去__proto__.__proto__上尋找,以此類推,直到找到一個值返回;若沒有則返回undefined。這種按照對象原型屬性尋找造成一個相似鏈狀的結構,叫作原型鏈。
clipboard.png
畫個圖表示:
clipboard.png
上圖中的__proto__紅線能夠理解爲原型鏈

這裏要注意的是,對象的原型屬性,保存的是對象的內存地址引用,須要讀取原型屬性的時候會找到該對象當時的狀態,因此更改原型鏈上原型屬性對象,會對該條原型鏈上的其餘對象形成影響。

繼承

ok通過這麼多鋪墊終於來到了繼承,繼承是面向對象裏面最重要的概念之一。咱們先來把相關概念介紹,再來看動手實現。

無論是實例,混入或者繼承,他們的誕生都是爲了解決同一個問題:代碼複用。只不過實現方式不一樣。

實例

這個是咱們平常開發中最經常使用的一種。

var date = new Date();

var instanceLightBox = new LightBox();

實例化一個對象能夠理解爲調用類的構造函數,返回一個擁有類全部屬性和方法的對象。

這樣說可能也不許確,咱們以var a = new A();爲例,實例化一個對象有幾個特色:

  1. a是一個object;
  2. a的構造函數是A;
  3. A構造函數中的非私有屬性會被a獲取到;
  4. A的原型是a的原型屬性;
function A () {
    this.a = 1;
};

A.prototype.getA = function(){
    return this.a;
}

var a = new A();

a.a; // 1
a.getA(); // 1

事實上咱們在上面已經講解了調用new關鍵字發生了什麼,這裏原理很少講。爲何要用實例化類:咱們能夠吧構造函數當作一個工廠,工廠產出了定製化模板(構造函數)和標準模板(構造函數的原型)的產品;咱們能夠經過屢次實例化一個類,產出多個同樣的產品,從而實現了代碼複用。

混入(mixin)

混入更像是一個加工廠,對已有的對象進行添加新屬性的操做。

function A (){
    this.a = 1;
};

// 一個很是簡單的mixin例子
function mixin(sourceObj, targetObj){
    for (var key in sourceObj) {
        // 只會在不存在的狀況下複製
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }
}

var a = new A();
var b = {b:2};
mixin(b, a);
a.b; // 2

這個例子能夠看到,targetObj混入了sourceObj的特有屬性,若是屬性是方法或者對象的話,targetObj保存的知識對象的引用,而不是本身獨有的屬性,這樣sourceObject更改targetObj也會跟着更改。

繼承(extend)

繼承裏面有兩個角色,父類和子類。繼承理解爲獲得父類全部的屬性,而且能夠重寫這些屬性。一樣是得到一個function所有的屬性和方法,我認爲實例和繼承的最大區別在於實例是構造函數實例對象,繼承是類繼承類,數據類型有明顯區別。

咱們先來看看ES6中的繼承:

class Parent {
    constructor (props) {
        const {name, phone} = props;
        this.name = name;
        this.phone = phone;
    }
       getInfo(){
        return this.name + ':' + this.phone;
    }
}

class Child extends Parent {
    constructor(props){
        super(props);
        const {gender} = props;
        this.gender = gender;
    }
    getNewInfo(){
        return this.name + ':' + this.gender + ':' + this.phone;
    }    
}

var childIns = new Child({
    name: 'aus',
    gender: 'male',
    phone: '1888888888'
});

先不討論繼承是如何實現的,先來看看繼承的結果。ES6中的繼承,Child類拿到了Parent類的構造器裏的非屬性和原型上的全部屬性,而且能夠擴展本身的私有屬性和原型屬性。可是父類和子類仍然公用父類的原型。

繼承有三個特色:

  1. 子類擁有父類非私有的屬性和方法。
  2. 子類能夠擁有本身屬性和方法,即子類能夠對父類進行擴展。
  3. 子類能夠用本身的方式實現父類的方法。

多態

這裏多態不詳細介紹,咱們來了解概念與實例。

多態:同一操做做用於不一樣的對象,能夠有不一樣的解釋,產生不一樣的執行結果。

舉個例子,父類原型上有個方法a,子類原型上有個同名方法a,這樣在子類實例上調用a方法必然是子類定義的a,可是我若是想用父類上的a怎麼辦。

class Parent {
    constructor (props) {
        const {name, phone} = props;
        this.name = name;
        this.phone = phone;
    }
       getInfo(){
        return this.name + ':' + this.phone;
    }
}

class Child extends Parent {
    constructor(props){
        super(props);
        const {gender} = props;
        this.gender = gender;
    }
    getInfo(from){
        // 全完自定義
        if('child' === from){
            return this.getNewInfo();
        } else {
            return super.getInfo();   
        }
    }
    getNewInfo(){
        return this.name + ':' + this.gender + ':' + this.phone;
    }    
}

var childIns = new Child({
    name: 'aus',
    gender: 'male',
    phone: '1888888888'
});
多態是一個很是普遍的話題,咱們如今所說的「相對」只是多態的一個方面:任何方法均可以引用繼承層次中高層的方法(不管高層的方法名和當前方法名是否相同)。之因此說「相對」是由於咱們並不會定義想要訪問的絕對繼承層次(或者說類),而是使用相對引用「查找上一層」。

繼承實現

一道很是常見的面試題,有多種方法,分紅兩個思路,篇幅有限,不過多介紹,詳細的文檔在github上,或者自行google。

參考

  1. 《JavaScript權威指南》
  2. 《JavaScript高級程序設計》
  3. 《你所不知道的JavaScript》
  4. JavaScript變量——棧內存or堆內存
  5. 內存管理
  6. 數據類型轉換
  7. 面向對象編程三大特性------封裝、繼承、多態
相關文章
相關標籤/搜索