JavaScript高級編程小結

Undefined

對未初始化的變量執行typeof操做符會返回undefined值,而對未聲明的變量執行typeof操做符一樣也會返回undefined算法

var message;
console.log(typeof message); // => undefined
console.log(typeof gaga); // => undefined

Boolean

各類類型轉換成Boolean的規則數組

數據類型 轉成true的值 轉成false的值
Boolean true false
String 任何非空字符串 ""空字符串
Number 任何非零數字值(包括無窮大) 0和NaN
Object 任何對象 null
Undefined n/a undefined

Number

Number類型應該是ECMAScript中最使人關注的數據類型了。瀏覽器

除了以十進制表示外,整數還能夠經過八進制或十六進制表示,其中,八進制字面值的第一位必須是0,而後是八進制數字序列(0 ~ 7)。若是字面值中的數值超出了範圍,那麼前導0將被忽略,後面的數值將被看成十進制數值解析app

var n = 070; // => 56
var n = 079; // => 79(無效的八進制數值)
var n = 08; // => 8(無效的八進制數值)

八進制字面量在嚴格模式下是無效的,會致使支持的JavaScript引擎拋出錯位。函數

十六進制字面值的前兩位必須是0x,後邊跟着任何十六進制數字(0 ~ 9 及 A ~ F)。其中,字母A ~ F 能夠大寫,也能夠小寫。性能

var n = 0xA; // 10
var n = 0x1f; // 31

計算的時候,八進制和十六進制都將轉成十進制後再計算。測試

因爲保存浮點數值須要的內存空間是保存整數值的兩倍,所以ECMAScript會不失時機的將浮點數值轉換爲整數值。this

永遠不要測試某個特定的浮點數值:prototype

if (a + b == 0.3) {
    alert("You got 0.3");
}

上邊的例子中,咱們測試的是兩個數的和是否是等於0.3。若是這兩個數是0.05和0.25,或者是0.15和0.15都不會有問題。若是這兩個數是0.1和0.2,那麼測試就沒法經過。指針

因爲內存的限制,ECMAScript並不能保存世界上全部的數值。若是某次計算的結果獲得了一個超出JavaScript數值範圍的值,那麼這個數值將被自動轉換成特殊的Infinity值,若是這個數值是負數,則會轉成-Infinity。出現正或負的Infinity值就不能繼續計算了。可使用isFinite()函數判斷一個數值是否是有窮的。

NaN是Not a Number的縮寫,它有兩個非同尋常的特色:

  • 任何涉及NaN的操做都會返回NaN
  • NaN與任何值都不相等,包括NaN自己

isNan()函數的原理是:在接受一個值後,會嘗試將這個值轉換成數值,成功就返回false,失敗則返回true。

有3個函數能夠把非數值轉換成數值:Number(),parseInt()和parseFloat()。Number函數能夠用於任何數據類型,另外兩個則專門用於把字符串轉換成數值。

Number()函數的轉換規則以下:

  • 若是是Boolean值,true和false將分別被轉換爲1和0
  • 若是是數字值,只是簡單的傳入和返回
  • 若是是null值,返回0
  • 若是是undefined,返回NaN
  • 若是是字符串,遵循下列規則:

    • 若是字符串中只包含數字(包括前面帶正好或負號的狀況),則將其轉換爲十進制數值,即「1」變成1,「123」會變成123,而「011」會變成11(注意:前導的0被忽略了)
    • 若是字符串中包含有效的浮點格式,如「1.1」,則將其轉換爲對應的浮點數值(一樣會忽略前導0)
    • 若是字符串中包含有效的十六進制格式,例如「0xf」,則將其轉換爲相同大小的十進制整數值
    • 若是字符串是空的(不包含任何字符),則將其轉換爲0
    • 若是字符串中包含除上述格式以外的字符,則將其轉換爲NaN
  • 若是是對象,則調用對象的valueOf()方法,而後依照前面的規則轉換返回的值,若是轉換的結果是NaN,則調用對象的toString()方法,而後再一次按照前面的規則轉換返回的字符串值。

var n = Number("Hello world"); // NaN
var n = Number(""); // 0
var n = Number("000011"); // 11
var n = Number("true"); // 1

parseInt()和parseFloat()在使用的時候須要特別注意進制的問題,parseFloat()只解析十進制。

String

String()方法內部轉換規則:

  • 若是值有toString()方法,則調用該方法並返回相應的結果,toString()方法不能處理null和undefined的狀況
  • 若是值是null,則返回「null」
  • 若是值是undefined,則返回「undefined」
var n1 = 10;
var n2 = true;
var n3 = null;
var n4;

console.log(String(n1));  // => "10"
console.log(String(n2));  // => "true"
console.log(String(n3));  // => "null"
console.log(String(n4));  // => "undefined"

邏輯與

邏輯與(&&)能夠應用於任何類型的操做數,而不只僅是布爾值。在有一個操做數不是布爾值的狀況下,邏輯與操做就不必定返回布爾值,它遵循下列規則:

  • 若是第一個操做數是對象,則返回第二個操做數
  • 若是第二個操做數是對象,則只有在第一個操做數的求值結果爲true的狀況下才會返回該對象
  • 若是兩個擦做數都是對象,則返回第二個操做數
  • 若是有一個操做數是null,則返回null
  • 若是有一個操做數是NaN,則返回NaN
  • 若是有一個操做數是undefined,則返回undefined

邏輯與操做屬於短路操做,即若是第一個操做數可以決定結果,那麼就不會再對第二個操做數求值,這個跟有些語言不同,所以在條件語句中使用邏輯與的時候要特別注意。

var n = true && NaN;
console.log(String(n)); // => NaN
var n2 = Boolean(n);
console.log(n2); // => false

if (!n) {
    console.log("ok"); // => ok
}

打印出了ok,說明在條件語句中可使用&&,可是須要明白返回值的問題。

相等操做符

相等(==)操做符在進行比較以前會對操做數進行轉換,咱們要了解這個轉換規則:

  • 若是有一個操做數是布爾值,則在比較相等性以前先將其轉換爲數值,false轉換爲0,而true轉換爲1
  • 若是一個操做數是字符串,另外一個操做數是數值,在比較相等性以前先將字符串轉換爲數值
  • 若是一個操做數是對象,另外一個操做數不是,則調用對象的valueOf()方法,用獲得的基本類型值按照前面的規則進行比較
  • null和undefined是相等的
  • 要比較相等性以前,不能將null和undefined轉換成其餘任何值
  • 若是有一個操做數是NaN,則相等操做符返回false,而不相等操做符返回true。重要提示:即便兩個操做數都是NaN,相等操做符也返回false
  • 若是兩個操做數都是對象,則比較他們是否是同一個對象。若是兩個操做數都指向同一對象,則相等操做符返回true,不然,返回false

全等(===)和相等(==)最大的不一樣之處是它不會對操做數進行強制轉換。

參數傳遞

ECMAScript中全部函數的參數都是按值傳遞的。也就是說,把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另外一個變量同樣。基本類型值的傳遞如同基本類型變量的複製同樣,而引用類型值的傳遞,則如同引用類型變量的複製同樣。有很多開發者在這一點上可能會感到困惑,由於訪問變量有按值和按引用兩種方式,而參數只能按值傳遞。

在向參數傳遞基本類型的值時,被傳遞的值會被複制給一個局部變量(即命名參數,或者用ECMAScript的概念來講,就是arguments對象中的一個元素)。在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量,所以這個局部變量的變化會反映在函數的外部。

先看一個基本類型值傳遞的例子:

function addTen(num) {
  num += 10;
  return num;
}

var count = 10;
var result = addTen(count);
console.log(count); // => 10
console.log(result); // => 20

上邊的代碼中,addTen函數並無改變count的值,按照上邊的理論,咱們能夠這麼看addTen函數:

function addTen(num) {
  num = count; // 當調用了函數的時候,函數內部作了這一個操做
  num += 10;
  return num;
}

再來看看引用類型的值傳遞的例子:

function setName(obj) {
  obj = person;  // 當調用了函數的時候,函數內部作了這一個操做
  obj.name = "James";
  obj = new Object();
  obj.name = "Bond";
}

var person = new Object();
setName(person);
console.log(person.name); // => "James"

在函數內部,一樣爲參數賦值了一個引用類型值的複製數據。在函數內部,obj就是一個指針,當給他從新賦值一個新的對象的時候,他指向了另外一個數據,所以,即便給它的name賦值,也不會影響函數外部的對象的值,說白了,仍是內存地址的問題。

Array

數組的length屬性頗有特色------他不是隻讀的。所以經過設置這個屬性,能夠從數組的末尾移除項或向數組中添加新項:

var colors = ["red", "blue", "green"];
colors.length = 2;
alert(colors[2]);  // => undefined

colors.length = 4;

上邊的代碼給colors設置了length後,最後邊的那個數據就變成了undefined,說明經過設置length可以修改數組的值,若是這個值大於數組元素的個數,那麼多出來的元素就賦值爲undefined。

數組的sort()方法會調用每一個數組項的toString()轉型防範,而後比較獲得的字符串,以肯定如何排序。即便數組中的每一項都是數值,sort()方法比較的也是字符串。 看個例子:

var array = [1, 4, 5, 10, 15];
array = array.sort();
console.log(array.toString()); // => 1,10,15,4,5

可見,即便例子中值的順序沒有問題,但sort()方法也會根據測試字符串的結果改變原來的順序。

數組有5種迭代方法:

  • every(): 對數組中的每一項運行給定函數,若是該函數對每一項都返回true,則返回true,就跟它的名字同樣,測試數組中是否每一項都符合函數的條件
  • some(): 對數組中的每一項運行給定函數,若是該函數對任一項返回true,則返回true,一樣,就跟它的名字同樣,測試數組中是否存在至少一項是符合函數的條件
  • filter(): 對數組中的每一項運行給定的函數,返回該函數會返回true的項組成的數組, 主要用於過濾數據
  • forEach(): 對數組中華的每一項運行給定函數,這個方法沒有返回值,就是遍歷方法
  • map(): 對數組中的每一項運行給定函數,返回每次函數調用的結果組成的數組,這個算是對數組中的項進行加工
var numbers = ["1", "2", "3", "4", "5", "6"];

// every() 檢測數組中的每一項是否都大於2
var everyResult = numbers.every(function (item, index, array) {
    return item > 2;
});
console.log(everyResult); // => false
    
// some() 檢測數組中是否至少有一項大於2
var someResult = numbers.some(function (item, index, array) {
    return item > 2;
});
console.log(someResult); // => true

// filter() 過濾數組中大於2的值
var filterResult = numbers.filter(function (item, index, array) {
    return item > 2;
});
console.log(filterResult); // => ["3", "4", "5", "6"]

// map() 加工數組中的數據
var maoResult = numbers.map(function (item, index, array) {
  return item * 2;
});
console.log(maoResult); // => [2, 4, 6, 8, 10, 12]

Function

使用函數做爲返回值是一件很奇妙的事情,咱們使用一個例子來看看:

function createComparisonFunction(propertyName) {
    return function (object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    }
}

var data = [{
    name: "zhangsan",
    age: 20
}, {
    name: "lisi",
    age: 30
}];

data.sort(createComparisonFunction("name"));
console.log(data[0]); // => {name: "lisi", age: 30}

data.sort(createComparisonFunction("age"));
console.log(data[0]); // => {name: "zhangsan", age: 20}

在函數內部,有兩個特殊的對象:arguments和this。其中,arguments是一個類數組對象,包含着傳入函數中的全部參數。雖然arguments的主要用途是保存函數參數,**但這個對象還有一個名叫callee的屬性,該屬性是一個指針,指向擁有這個arguments對象的函數,咱們看下邊這個很是經典的階乘函數:

function factorial(num) {
    if (num < 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}

console.log(factorial(5)); // => 120

定義階乘函數通常都要用到遞歸算法,如上邊的代碼所示,在函數有名字,並且名字之後都不會變的的狀況下,這樣定義沒問題。但問題是這個函數的執行與函數名factorial僅僅耦合在了一塊兒。爲了消除這種緊密耦合的現象,能夠像下面這樣是喲很難過arguments.callee:

function factorial(num) {
    if (num < 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}

console.log(factorial(5)); // => 120

咱們修改factorial函數的實現後:

const anotherFactorial = factorial;
factorial = function () {
    return 0;
}

console.log(anotherFactorial(5)); // => 120
console.log(factorial(5)); // => 0

使用call()或apply()來擴充做用域的最大好處,就是對象不須要與方法有任何耦合關係。

屬性類型

ECMAScript中有兩種屬性:數據屬性和訪問器屬性。

1.數據屬性

數據屬性包含一個數據值的位置。在這個位置能夠讀取和寫入值。數據屬性有4個描述其行爲的特性:

  • configurable
  • enumerable
  • writable
  • value

咱們先看幾個例子:

const person = { };
Object.defineProperty(person, "name", {
    writable: false,
    value: "James"
});
console.log(person.name); // => James
person.name = "Bond";
console.log(person.name); // => James

上邊的代碼設置了person中的屬性name的特性,把它的writable設置爲false,所以當咱們重寫它的name屬性的時候是不起做用的,使用value能夠給屬性賦值。咱們再看一個例子:

const person = { };
Object.defineProperty(person, "name", {
    configurable: false,
    value: "James"
});
console.log(person.name); // => James
delete person.name;
console.log(person.name); // => James

當咱們把confugurable設置爲false的時候,就把name屬性的可配置性給鎖死了,一旦把confugurable設爲false,後續的再次對這個屬性設置特性的時候就會出錯。下邊的代碼會報錯:

Object.defineProperty(person, "name", {
    writable: true,
    value: "JJJJJ"
});
console.log(person.name);

2.訪問器屬性

訪問器屬性不含數據值,但能夠經過set或get方法來設置或獲取值,就像制定了一套這樣的規則。我跟喜歡稱這個特性爲計算屬性

const book = {
    _year: 2004,
    edition: 1
};

Object.defineProperty(book, "year", {
    get: function () {
        return this._year;
    },
    
    set: function (newValue) {
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

book.year = 2005;
console.log(book.edition);

在這個例子中。_year很想一個私有變量,咱們經過set,get方法來寫了一個year屬性,固然也可使用這種方式來控制屬性是否只讀或只寫特性。

有一點值得注意,上邊說的這些內容算是爲對象建立屬性的方法,咱們也能夠採用person.name這種方式建立屬性,只不事後邊這種建立的方式給裏邊的特性賦了默認的值。

建立對象

JavaScript中Object的總結這篇文章中,我介紹了多種建立對象的方法:

工廠方法

核心思想是經過函數來建立對象,函數會返回一個根據參數建立的新的對象,這個方法雖然解決了建立多個類似對象的問題,但沒有解決對象識別的問題,由於在函數內容,知識把參數賦值給了任何對象的屬性

構造函數

構造函數的使用方法我就不提了,我只說幾點須要注意的地方,構造函數的第一個字母要大寫,內部使用this來指定屬性和方法。在建立對象的時候要加上new關鍵字。

其實構造函數的本質也是一個函數,若是在調用的時候不加關鍵字new,那麼它內部的屬性將會建立爲全局變量的屬性。**任何加上new關鍵字的函數都會變成構造函數,而構造函數的本質是:

var a = {};
a.__proto__ = F.prototype;
F.call(a);

構造函數可以讓咱們經過相似.constructor或instanceof來判斷對象的類型,但它的缺點是會爲相同的屬性或方法建立重複的值,咱們都知道在JavaScript中函數也是對象,這種返回建立統一對象的過程,確定給性能帶來了很大的挑戰,所以這種模式還須要升級。

原型模式

原型模式是很是重要的一個概念,咱們會使用很長的篇幅來介紹這方面的內容。

首先咱們應該明白函數名字本質上是一個指向函數對象的指針,所以他能表示這個函數對象,在JavaScript中每一個函數**內部都有一個prototype屬性,這個屬性是一個指針,指向一個對象,而這個對象的用因而包含屬性和方法。所以咱們有這樣的啓發,若是我給構造函數的prototype賦值屬性和方法,那麼我在使用構造函數建立對象的時候,是否是就能夠繼承這些共有的屬性呢? 答案是確定的:

function Person() {
    
}

Person.prototype.name = "James";
Person.prototype.sayName = function () {
    console.log(this.name);
};

const person1 = new Person();
person1.sayName(); // => James

const person2 = new Person();
person2.sayName(); // => James


console.log(person1.name == person2.name); // => true

1.理解原型對象

不管何時,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性。這個屬性指向函數的原型西鄉,在默認狀況下,這個prototype又會自動獲取一個叫作constructor的屬性,這個屬性包含一個指向prototype屬性所在函數的指針,能夠說這是一個迴路。

那麼建立一個實例的過程是怎麼樣的呢?

當咱們用構造函數建立一個實例後,該實例內部也會有一個指針指向構造函數的原型對象,通常狀況下,這個指針的名字並非prototype,咱們必須記住一點,prototype只是函數內部的一個屬性。大部分瀏覽器的這個指針是__proto__咱們看一張圖:

上圖很好的展現了構造函數和實例對象之間原型的關係。咱們在這裏就不一一說明了。雖然咱們經過__proto__能訪問到原型對象,但這絕對不是推薦作法。咱們能夠經過isPrototypeOf()方法來肯定對象之間是否存在這種關係:

console.log(Person.prototype.isPrototypeOf(person1)); // => true

上邊的代碼很好的演示了這一說法,實例對象person1的原型就是構造函數Person的prototype。,還有一個方法是獲取原型對象getPrototypeOf()

console.log(Object.getPrototypeOf(person1) == Person.prototype); // => true

每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性。搜索首先從對象實例自己開始。若是在實例中找到了具備給定名字的屬性,則返回該屬性,若是沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具備給定名字的屬性,若是在原型對象中找到這個屬性,則返回該屬性。具體的例子咱們就不演示了。

值得注意的是,當給對象的屬性賦值時,若是屬性的名稱與原型對象的屬性名稱相同,對象內部會建立這個屬性,原型中的屬性保持不變。,咱們能夠這麼認爲,原型對象大部分時候只提供讀取功能,它的目的是共享數據。但若是給引用類型的屬性賦值的時候會有不一樣的狀況,好比修改原型的對象,數組就會致使原型的數據遭到修改。這個在JavaScript中Object的總結這篇文章中我已經詳細的給出瞭解釋

2.原型與in操做符

經過上邊的距離,咱們大概明白了對象屬性與原型之間的關係,那麼如今就引出了一個問題。如何區分某個屬性是來自對象自己仍是原型呢?爲了解決這個問題,咱們引出in操做符。

有兩種方式使用in操做符:單獨使用和在for-in循環中使用。在單獨使用時,in操做符會在經過對象可以訪問給定屬性時返回true,不管該屬性存在於實例中仍是原型中。咱們看下邊這個例子:

function Person() {
    
}

Person.prototype.name = "James";
Person.prototype.sayName = function () {
    console.log(this.name);
};

const person1 = new Person();
console.log(person1.hasOwnProperty("name")); // => false
console.log("name" in person1); // => true

hasOwnProperty()方法可以判斷對象自己是否存在某個屬性,而in可以判斷對象是否可以訪問某個屬性,結合這兩種方法,咱們就能判斷某個屬性的來源,咱們舉個簡單的例子:

function hasPrototypeProperty(object, name) {
    return (!object.hasOwnProperty(name)) && (name in object);
}

console.log(hasPrototypeProperty(person1, "name")); // => true

for-in能夠遍歷對象中的屬性,**可是要依賴屬性中的enumerable這個特性的值,若是這個值爲false,那麼就沒法遍歷到屬性,跟for-in很類似的方式是Object.keys()他返回一個字符串數組,若是要想遍歷出對象的屬性,忽略enumerable的影響,可使用Object.getOwnPropertyNames()這個方法,下邊是一個簡單的例子:

function Person() {
    
}

Person.prototype.name = "James";
Person.prototype.sayName = function () {
    console.log(this.name);
};

const person1 = new Person();

console.log(hasPrototypeProperty(person1, "name")); // => true

Object.defineProperty(person1, "age", {
    enumerable: false
});

for (const pro in person1) {
    console.log(pro);
}

const keys = Object.keys(person1);
console.log(keys);

const keys1 = Object.getOwnPropertyNames(person1);
console.log(keys1);

3.原型的動態性

在上邊的內容中,咱們已經明白,JavaScript中尋找屬性或方法是經過搜索來實現的,所以咱們能夠動態的爲原型添加屬性和方法。這一方面沒什麼好說的,但有一點值得注意,若是把原型修改成另外一個對象,就會出現問題。,仍是先看一個實例:

function Person() {
    
}

const person = new  Person();

Person.prototype = {
    constructor: Person,
    name: "James",
    sayName: function () {
        console.log(this.name);
    }
};

console.log(person.sayName()); // 會報錯

上邊的代碼會報錯,根本緣由是對象的原型對象指向了原型,而不是指向了構造函數,這就比如這樣的代碼:

var person1 = person;
var person2 = person;
person1 = son;

上邊的代碼中,person1換了一個對象,可是person2依然指向了person。用下邊這個圖開看更直接

4.原生對象的原型

這一小節是一個很重要的小結,咱們慢慢的增長了對JavaScript語言的理解。原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型(Object,Array,String等等)都在器構造函數的原型上定義了方法。

alert(typeof Array.prototype.sort); // => function

所以咱們就經過這種手段爲原生的引用類型擴展更多的屬性和方法。

String.prototype.startsWith = function (text) {
    return this,indexOf(text) == 0;
}

這種方式很是像面嚮對象語言中的分類,分類使用好了,可以增長程序的可讀性,但在JavaScript中,不建議用這種方法爲原生對象作擴展。由於這麼作的後果是可能讓程序失控。

相關文章
相關標籤/搜索