對未初始化的變量執行typeof操做符會返回undefined
值,而對未聲明的變量執行typeof操做符一樣也會返回undefined
算法
var message; console.log(typeof message); // => undefined console.log(typeof gaga); // => undefined
各類類型轉換成Boolean的規則數組
數據類型 | 轉成true的值 | 轉成false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | ""空字符串 |
Number | 任何非零數字值(包括無窮大) | 0和NaN |
Object | 任何對象 | null |
Undefined | n/a | undefined |
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的縮寫,它有兩個非同尋常的特色:
isNan()函數的原理是:在接受一個值後,會嘗試將這個值轉換成數值,成功就返回false,失敗則返回true。
有3個函數能夠把非數值轉換成數值:Number(),parseInt()和parseFloat()。Number函數能夠用於任何數據類型,另外兩個則專門用於把字符串轉換成數值。
Number()函數的轉換規則以下:
若是是字符串,遵循下列規則:
若是是對象,則調用對象的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()方法內部轉換規則:
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"
邏輯與(&&)能夠應用於任何類型的操做數,而不只僅是布爾值。在有一個操做數不是布爾值的狀況下,邏輯與操做就不必定返回布爾值,它遵循下列規則:
邏輯與操做屬於短路操做,即若是第一個操做數可以決定結果,那麼就不會再對第二個操做數求值,這個跟有些語言不同,所以在條件語句中使用邏輯與的時候要特別注意。
var n = true && NaN; console.log(String(n)); // => NaN var n2 = Boolean(n); console.log(n2); // => false if (!n) { console.log("ok"); // => ok }
打印出了ok,說明在條件語句中可使用&&,可是須要明白返回值的問題。
相等(==)操做符在進行比較以前會對操做數進行轉換,咱們要了解這個轉換規則:
全等(===)和相等(==)最大的不一樣之處是它不會對操做數進行強制轉換。
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賦值,也不會影響函數外部的對象的值,說白了,仍是內存地址的問題。
數組的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種迭代方法:
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 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中有兩種屬性:數據屬性和訪問器屬性。
數據屬性包含一個數據值的位置。在這個位置能夠讀取和寫入值。數據屬性有4個描述其行爲的特性:
咱們先看幾個例子:
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);
訪問器屬性不含數據值,但能夠經過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
不管何時,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個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的總結這篇文章中我已經詳細的給出瞭解釋。
經過上邊的距離,咱們大概明白了對象屬性與原型之間的關係,那麼如今就引出了一個問題。如何區分某個屬性是來自對象自己仍是原型呢?爲了解決這個問題,咱們引出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);
在上邊的內容中,咱們已經明白,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。用下邊這個圖開看更直接
這一小節是一個很重要的小結,咱們慢慢的增長了對JavaScript語言的理解。原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型(Object,Array,String等等)都在器構造函數的原型上定義了方法。
alert(typeof Array.prototype.sort); // => function
所以咱們就經過這種手段爲原生的引用類型擴展更多的屬性和方法。
String.prototype.startsWith = function (text) { return this,indexOf(text) == 0; }
這種方式很是像面嚮對象語言中的分類,分類使用好了,可以增長程序的可讀性,但在JavaScript中,不建議用這種方法爲原生對象作擴展。由於這麼作的後果是可能讓程序失控。