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將被忽略,后面的數值將被當作十進制數值解析
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會不失時機的將浮點數值轉換為整數值。
永遠不要測試某個特定的浮點數值:
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中,不建議用這種方法為原生對象做擴展。因為這么做的后果是可能讓程序失控。