跨域主要是由於瀏覽器的“同源策略”引起,分為多種類型,本文主要探討Ajax請求跨域問題
前言
強烈推薦閱讀參考來源中的文章,能夠快速幫助了解跨域的原理
參考來源
本文參考了以下來源
什么是跨域
為了更了解跨域的原理,可以閱讀參考來源中的文章,里面對跨域的原理講解很詳細到位
ajax跨域的表現
ajax請求時,如果存在跨域現象,並且沒有進行解決,會有如下表現
- 第一種現象:
No 'Access-Control-Allow-Origin' header is present on the requested resource,並且The response had HTTP status code 404,如圖出現這種情況的原因如下
- 本次ajax請求是“非簡單請求”,所以請求前會發送一次預檢請求(OPTIONS)
- 服務器端后台接口沒有允許OPTIONS請求,導致無法找到對應接口地址
- 第二種現象:
No 'Access-Control-Allow-Origin' header is present on the requested resource和The response had HTTP status code 405,如圖這種現象和第一種有區別,這種情況下,后台方法允許OPTIONS請求,但是一些配置文件中(如安全配置),阻止了OPTIONS請求,才會導致這個現象
- 第三種現象:
No 'Access-Control-Allow-Origin' header is present on the requested resource,如圖這種現象和第一種和第二種有區別,這種情況下,服務器端后台允許OPTIONS請求,並且接口也允許OPTIONS請求,但是頭部匹配時出現不匹配現象 比如origin頭部檢查不匹配,比如少了一些頭部的支持(如常見的X-Requested-With頭部),然后服務端就會將response返回給前端,前端檢測到這個后就觸發XHR.onerror,導致前端控制台報錯
- 注意,一般進行跨域分析的請求都是在ajax請求出現跨域情況的,而且用普通的http請求不會出現問題的
跨域的原理
之所以ajax出現請求跨域錯誤問題,主要原因就是因為瀏覽器的“同源策略”,可以參考 瀏覽器同源政策及其規避方法(阮一峰)
如何解決跨域問題
一般ajax跨域解決就是通過JSONP解決或者CROS解決,如以下(注意,ajax跨域只是屬於瀏覽器"同源策略"中的一部分,其它的還有Cookie跨域iframe跨域,LocalStorage跨域等這里不做介紹)
JSONP方式解決跨域問題
jsonp解決跨域問題是一個比較古老的方案(實際中不推薦使用),這里做簡單介紹
實際項目中如果要使用JSONP,一般會使用JQ等對JSONP進行了封裝的類庫來進行ajax請求
實現原理
JSONP之所以能夠用來解決跨域方案,主要是因為 <script> 腳本擁有跨域能力,而JSONP正是利用這一點來實現。具體原理如圖
實現流程
JSONP的實現步驟大致如下(參考了來源中的文章)
- 客戶端網頁網頁通過添加一個<script>元素,向服務器請求JSON數據,這種做法不受同源政策限制
function addScriptTag(src) { var script = document.createElement('script'); script.setAttribute("type","text/javascript"); script.src = src; document.body.appendChild(script); } window.onload = function () { addScriptTag('http://example.com/ip?callback=foo'); } function foo(data) { console.log('response data: ' + JSON.stringify(data)); };請求時,接口地址是作為構建出的腳本標簽的src的,這樣,當腳本標簽構建出來時,最終的src是接口返回的內容
- 服務端對應的接口在返回參數外面添加函數包裹層
foo({ "test": "testData" }); - 由於<script>元素請求的腳本,直接作為代碼運行。這時,只要瀏覽器定義了foo函數,該函數就會立即調用。作為參數的JSON數據被視為JavaScript對象,而不是字符串,因此避免了使用JSON.parse的步驟。
注意,一般的JSONP接口和普通接口返回數據是有區別的,所以接口如果要做JSONO兼容,需要進行判斷是否有對應callback關鍵字參數,如果有則是JSONP請求,返回JSONP數據,否則返回普通數據
使用注意
基於JSONP的實現原理,所以JSONP只能是“GET”請求,不能進行較為復雜的POST和其它請求,所以遇到那種情況,就得參考下面的CROS解決跨域了
CROS解決跨域問題
CORS是一個W3C標准,全稱是"跨域資源共享"(Cross-origin resource sharing)。它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
基本上目前所有的瀏覽器都實現了CORS標准,其實目前幾乎所有的瀏覽器ajax請求都是基於CORS機制的,只不過可能平時前端開發人員並不關心而已(所以說其實現在CROS解決方案主要是考慮后台該如何實現的問題)。
強烈推薦閱讀 跨域資源共享 CORS 詳解(阮一峰)
CROS請求原理
實現原理如下圖(簡化版本)
- 如何判斷是否是簡單請求?
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。只要同時滿足以下兩大條件,就屬於簡單請求。
- 請求方法是以下三種方法之一:
HEAD,GET,POST
- HTTP的頭信息不超出以下幾種字段:
Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
- 請求方法是以下三種方法之一:
PHP后台配置
PHP后台得配置幾乎是所有后台中最為簡單的,遵循如下步驟即可
- 第一步:配置Php 后台允許跨域
主要為跨域CROS配置的兩大基本信息,Origin和headers
- 第二步:配置Apache web服務器跨域
一般每一個web服務器安裝好都得配置下跨域信息
以上這張圖片是在某一個網站上看到的,直接用來。但是具體的源地址已經找不到了...
JAVA后台配置
JAVA后台相對來說配置也比較簡單,只需要遵循如下步驟即可
- 第一步:獲取依賴jar包
下載 cors-filter-1.7.jar, java-property-utils-1.9.jar 這兩個庫文件放到lib目錄下。(放到對應項目的webcontent/WEB-INF/lib/下)
- 第二步:如果項目用了Maven構建的,請添加如下依賴到pom.xml中:(非maven請忽視)
<dependency> <groupId>com.thetransactioncompany</groupId> <artifactId>cors-filter</artifactId> <version>[ version ]</version> </dependency>
其中版本應該是最新的穩定版本,CROS過濾器
- 第三步:添加CROS配置到項目的Web.xml中( App/WEB-INF/web.xml)
<!-- 跨域配置--> <filter> <!-- The CORS filter with parameters --> <filter-name>CORS</filter-name> <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class> <!-- Note: All parameters are options, if omitted the CORS Filter will fall back to the respective default values. --> <init-param> <param-name>cors.allowGenericHttpRequests</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>cors.allowOrigin</param-name> <param-value>*</param-value> </init-param> <init-param> <param-name>cors.allowSubdomains</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>cors.supportedMethods</param-name> <param-value>GET, HEAD, POST, OPTIONS</param-value> </init-param> <init-param> <param-name>cors.supportedHeaders</param-name> <param-value>Accept, Origin, X-Requested-With, Content-Type, Last-Modified</param-value> </init-param> <init-param> <param-name>cors.exposedHeaders</param-name> <!--這里可以添加一些自己的暴露Headers --> <param-value>X-Test-1, X-Test-2</param-value> </init-param> <init-param> <param-name>cors.supportsCredentials</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>cors.maxAge</param-name> <param-value>3600</param-value> </init-param> </filter> <filter-mapping> <!-- CORS Filter mapping --> <filter-name>CORS</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
請注意,以上配置文件請放到web.xml的前面,作為第一個filter存在(可以有多個filter的)
NET后台配置
.NET后台配置相比前面兩者,復雜一點,可以參考如下步驟
- 第一步:網站配置
打開控制面板,選擇管理工具,選擇iis;右鍵單擊自己的網站,選擇瀏覽;打開網站所在目錄,用記事本打開web.config文件添加下述配置信息,重啟網站
請注意,以上截圖較老,如果配置仍然出問題,可以考慮增加更多的headers允許,比如
"Access-Control-Allow-Headers":"X-Requested-With,Content-Type,Accept"
或者添加更多的自定義頭部
- 第二步:其它更多配置
如果第一步進行了后,仍然有跨域問題,可能是因為后台的接口沒有運行OPTIONS請求的原因,請將
[System.Web.Mvc.HttpPost]去掉,讓接口支持更多請求
FAQ
multi value '*,*' 的問題
-
出現原因
這個問題出現的原因是由於后台響應的http頭部信息有兩個Access-Control-Allow-Origin:*。常見於.net后台(一般在web.config中配置了一次origin,然后代碼中又手動添加了一次origin)
-
表現現象
如圖
-
解決方法
將代碼中手動添加的Origin:*去掉(注意,如果去除config中的配置,會導致跨域問題,只有去除代碼中自己加上的才行...)
說明
介紹JavaScript數據類型
目錄
前言
參考來源
前人栽樹,后台乘涼,本文參考了以下來源
前置技術要求
閱讀本文前,建議先閱讀以下文章
JavaScript的6種數據類型
var 變量 = 值; //其中只有兩種類型,一種是基本類型(類似於常量數據),一種是引用類型(對象)
首先,我們要明確一點JavaScript的數據類型即為值的數據類型。JavaScript中有6種數據類型(5種基本數據類型,1種引用類型)
哪6種數據類型
- 五種基本數據類型(其實也統稱為基本型或原始型)
undefined,null,number,boolean,string
- 一種復雜數據類型(引用型)
Object
undefined 類型
undefined型只有一個值,即特殊的undefined。使用var聲明變量但未對其加以初始化時,這個變量的值就就是undefined。例如
var a; console.log(a===undefined);//true
null 類型
null型也只有一個值,即null,從邏輯角度來看,null值表示一個空指針(這也是 使用typeof操作符檢測返回object的原因)。如果定義的變量准備在將來用於保存對象,那么最好將該變量初始化為null而不是其它值。這樣只要直接檢測null值就可以知道相應的變量是否已經保存了一個對象的引用了。
var a = null; console.log(typeof a);//"object"
實際上,ECMAScript中,可以認為undefined值是派生自null值的,所以undefined==null但是undefined!==null
console.log(undefined==null);//true console.log(undefined===null);//false
注意,一定要區分undefined和null的使用。一般來說,變量的值初始設置為undefined(記得別顯示設置,由解釋器默認設置即可)。而引用型對象的初始值一般可以顯式的設置為null。或者可以這樣理解undefined作為基本數據類型的變量的初始值(默認設置),null作為引用類型的變量的初始值(顯式設置)
boolean 類型
boolean型只有兩個字面值true,false。但是這兩個值和數字值不是一回事,因此true不一定等於1,而false也不一定等於0。
要將一個值轉為boolean型等價的值,有兩種方案:
- 一種是顯式轉換-調用類型轉換函數Boolean()
- 一種是自動轉換-如if(value)中的value就會自動轉化成為boolean值
各類型與boolean型之間值得轉換關系如下表
| 數據類型 | 轉換為true的值 | 轉換為false的值 |
|---|---|---|
| boolean | true | false |
| string | 任何非空字符串 | "" (空字符串) |
| bumber | 任何非零數字值(包括無窮大) | 0和NaN |
| undefined | 無 | undefined |
| null | 無 | null |
| Object | 任何對象 | 無 |
number 類型
number類型用來表示整型和浮點數字,還有一種特殊的數值(NaN-not a number,這個數值用於表示一個本來要返回數值的操作數未返回數值得情況-防止拋出錯誤)。
比如在其它語言中數值÷0都會導致錯誤,停止運行,但是在JS中。0/0、NaN/0會返回NaN,其它數字/0會返回Infinity,不會報錯。
任何涉及與NaN的操作都會返回NaN,JS有一個isNaN()函數,可以判斷接收的參數是否為NaN,或者參數轉化為數字后是否為NaN
console.log(NaN + 1); //NaN,任何涉及到NaN的操作都會返回NaN
console.log(NaN === NaN); //false,NaN與任何值都不相等,包括NaN本身
console.log(isNaN(NaN)); //true,是NaN
console.log(isNaN('10')); //false,被轉為數字10
console.log(isNaN(true)); //false,被轉為數字1
console.log(isNaN(null)); //false,被轉為數字0
console.log(isNaN(undefined)); //true,返回NaN
console.log(isNaN('hello')); //true,無法轉換為數字
console.log(0/0);//NaN,0/0返回NaN
console.log(NaN/0);//NaN,NaN/0返回NaN
console.log(1/0);//Infinity,其它數字/0返回Infinity
console.log('1'/0);//Infinity,'1'成功轉為數字
console.log('1a'/0);//NaN,'1a'轉為數字失敗,變為NaN
console.log(Infinity/0);//Infinity,其它數字/0返回Infinity
注意:Infinity的類型是Number(不是基礎數據類型)
有兩種方法可以將非number類型的值轉換為number類型
- 一種是隱式轉換,如進行(*、/)操作時,會自動其余類型的值轉為number類型
console.log("1"*2);//12 console.log("1"/2);//0.5 console.log("1a"/2);//NaN - 一種是顯示轉換-調用Number()、parseInt()、parseFloat()方法轉換
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()方法,然后再依次按照前面的規則轉換返回的字符串值。
console.log(Number(''));//0 console.log(Number('a'));//NaN console.log(Number(true));//1 console.log(Number('001'));//1 console.log(Number('001.1'));//1.1 console.log(Number('0xf'));//15 console.log(Number('000xf'));//NaN var a = {}; console.log(Number(a));//NaN a.toString = function(){return 2}; console.log(Number(a));//2 a.valueOf = function(){return 1}; console.log(Number(a));//1
parseInt()常常用於將其它類型值轉化為整形。parseInt轉換與Number()有區別,具體規則如下
- parseInt(value,radius)有兩個參數,第一個參數是需要轉換的值,第二個參數是轉換進制(該值介於 2 ~ 36 之間。如果該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN。),如果不傳(或值為0),默認以10為基數(如果value以 “0x” 或 “0X” 開頭,將以 16 為基數)
- 注意在第二個參數默認的情況下,如果需要轉換的string值以0開頭,如'070',有一些環境中,會自動轉化為8進制56,有一些環境中會自動轉化為10進制70。所以為了統一效果,我們在轉換為10進制時,會將第二個參數傳10
- parseInt轉換示例
console.log(parseInt(''));//NaN console.log(parseInt('a'));//NaN console.log(parseInt('1234blue'));//1234 console.log(parseInt(true));//NaN console.log(parseInt('070'));//70,但是有一些環境中會自動轉換為8進制56 console.log(parseInt('070',8));//56 console.log(parseInt('001.1'));//1 console.log(parseInt('0xf'));//15,16進制 console.log(parseInt('AF',16));//175,16進制 console.log(parseInt('AF'));//NaN console.log(parseInt('000xf'));//0 var a = {}; console.log(parseInt(a));//NaN a.toString = function(){return 2}; console.log(parseInt(a));//2 a.valueOf = function(){return 1}; console.log(parseInt(a));//2
parseFloat()轉換規則基本與parseInt()一致,只有如下不同點
- parseFloat()遇到浮動數據時,浮點有效(但是只有第一個.有效),如"10.1"會被轉為10.1;'10.1.1'會被轉為10.1
- parseFloat()只會默認處理為10進制,而且會忽略字符串前面的0,所以不會有在默認情況下轉為8進制的情況
- 示例
console.log(parseFloat('1234blue'));//1234 console.log(parseFloat('1234blue',2));//1234 console.log(parseFloat('0xA'));//0 console.log(parseFloat('10.1'));//10.1 console.log(parseFloat('10.1.1'));//10.1 console.log(parseFloat('010'));//10
由於Number()函數在轉換字符串時比較復雜而且不夠合理,因此在處理整數的時候更常用的是parseInt()函數-需注意最好第二個參數傳10,處理浮點數時更常用parseFloat()
另外注意,浮點數直接的計算存在誤差,所以兩個浮點數無法用"="進行判斷
var a=10.2; var b= 10.1; console.log(a - b === 0.1);//false console.log(a - 10.1 === 0.1);//false,實際是0.09999999999999964 console.log(a - 0.1 === 10.1);//true
string 類型
string類型用於表示由零或多個16位Unicode字符組成的字符序列,即字符串。字符串可以由單引號(')或雙引號(")表示。任何字符串的長度都可以通過訪問其length屬性取得。
要把一個值轉換為一個字符串有三種方式。
- 第一種是使用幾乎每個值都有的toString()方法(除去null和undefined沒有)
- toString(radius)有一個參數-基數,當需要toString的值為number時,參數可以生效(可以轉換為對應進制輸出,如10.toString(8)輸出為12)
- 第二種是隱式轉換,比如字符串+ number(null,undefined,object等),會默認轉為字符串(如果后面相加的是object對象,會返回object對象的toString或valueOf值)
- 第三種是通過轉換函數String(),String()轉換規則如下
- 如果值有toString()方法,則調用該方法(沒有參數)並返回相應的結果(注意,valueOf()方法沒用)
- 如果值是null,則返回"null"
- 如果值是undefined,則返回”undefined“
- 示例
var a = 10; var b = '10' var c = {}; console.log(a.toString());//10 console.log(a.toString(8));//12 console.log(b.toString(8));//10,字符串基數沒用 console.log(String(c));//[object Object] console.log(c);//[object Object] console.log(c + '1');//[object Object]1 c.valueOf = function(){return 2}; console.log(String(c));//[object Object] console.log(c);//[object Object],valueOf沒用 console.log(c + '1');//21,隱式轉換時,valueOf起作用了 c.toString = function(){return 2}; console.log(String(c));//2 console.log(c);//2,toString起作用了 console.log(String(null));//"null",null和undefined可以String()輸出 console.log(null.toString());//報錯,null和undefined不能toString
復雜 類型
復雜 類型即引用型,也就是我們常說的JS對象(包括普通的object型對象和function型對象)
對象其實就是一組數據和功能的集合。對象可以通過執行new操作符后跟要創建的對象類型的名稱來創建。而創建Object的實例並為其添加屬性和(或)方法,就可以創建自定義對象。如
var o = new Object();//創建一個新的自定義對象{}
也就是說,除去基本類型,剩余的就是引用型(包括內置對象,自定義對象等)都是基於Object進行拓展的
Object的每個實例都具有下列屬性和方法:
- constructor——保存着用於創建當前對象的函數
- hasOwnProperty(propertyName)——用於檢查給定的屬性在當前對象實例中(而不是在實例的原型中)是否存在。其中,作為參數的屬性名(propertyName)必須以字符串形式指定(例如:o.hasOwnProperty("name"))
- isPrototypeOf(object)——用於檢查傳入的對象是否是另一個對象的原型
- propertyIsEnumerable(propertyName)——用於檢查給定的屬性是否能夠使用for-in語句來枚舉
- toString()——返回對象的字符串表示
- valueOf()——返回對象的字符串、數值或布爾值表示。通常與toString()方法的返回值相同。
參考 JS原型和原型鏈的理解
基本型和引用型的不同
基本型和引用型最大的不同就是兩者的存儲方式不同,如圖:
-
也就是說,上圖中,如果變量1的值變為102,實際中棧內存中的101是不會變的,只是在棧內存中新開辟出一處,用來存放102這個常量。然后將變量1指向102。
-
而變量2由於棧內存中存放的是指針,實際執行的是堆內存中的數據,所以變量2的值是可以隨便改的(堆內存中的數據可以更改)
關於數據類型的一些常見疑問
為什么typeof null === 'object'
這個問題有很多人提出過,因為按理說,null作為JS的五大基本數據類型之一,那么typeof null 為和會===object呢?這與ECMAScript的歷史原因有關。原因如下:
- JS中的五大基本數據類型,除了null外,其余的類型存放在棧區的都是常量值(如undefined存放的是undefined值,number類型可以存放0,1,2...等等)
- 與其余四種類型相同,null的值也是存放在棧區的,而且也只有一個值null。而恰巧從邏輯上認為這是一個空對象的指針(機器碼NULL空指針),所以typeof時會返回object。 (具體原因如下,引自知乎同名回答)
- JS類型值是存在32 BIT 單元里,32位有1-3位表示TYPE TAG,其它位表示真實值
- 而表示object的標記位正好是低三位都是0 (000: object. The data is a reference to an object.)
- 而js 里的null 是機器碼NULL空指針, (0x00 is most platforms).所以空指針引用 加上 對象標記還是0,最終體現的類型還是object..
- 這也就是為什么Number(null)===0吧...
- 曾有提案嘗試修復typeof === 'null',但是被拒絕了(如在V8引擎中會導致大量問題)
string,String,object,Object,function,Function的關系
請區分Object,Function,String與object,function,string。
- Object,Fucntion,String是JS內置對象(都是引用型),object,function,string是typeof檢查類型后的返回值。
- 一般情況下,我們把后面的object,undefined,function等稱之為對應值的類型(null用typeof 無法識別的,另外函數對象返回function)。
- 所以到了這一步,應該是所有的引用類型typeof都返回object的。但是在引用類型中,有一個比較特殊的類型"fucntion"。它的出現造成了引用類型中函數對象typeof返回'function'。
具體參考: function類型與object類型的區別
- 現在又回到了最初的數據類型划分的時候了
- JS中的基本數據類型有五種:undefined,null,number,boolean,string
- JS中的引用類型中包含兩種:object、function(fucntion類型是Function對象typeof檢測后的返回值,Function對象是基於Object拓展的)
- JS中有一些內置對象:Object,Function,String。這些對象用typeof返回值都是function。但是new Object()出來的新對象的typeof返回值就是object(除了new Function()返回function以外)
- 所以其實這些類型名稱都是不同人自己定義出來的,不要被他們限制。
比如有的人會說JS中有7中類型:5中基本數據類型和object與function(但其實我們這這里就將后面兩種以前算成引用型了)
或者用一句話總結更為合適:"JS中有對象,每一個對象都有一個自己的類型"。就好比每一個動物都有屬於自己的類型一樣(人類,猴子...)。另外基本類型可以認為是一個不會改變的對象(便於理解)
至於為什么基本類型明明不是引用型,卻能像引用型一樣去使用一些基本數據操作(如toFixed,toString等)。請參考 基本數據類型為什么能夠使用toString等操作
關於String類型與string類型的疑問
JavaScript中,基本類型有string,number等等,復雜類型中也拓展有String,Number等等。那么這兩者的區別是什么呢?如下圖,簡單描述了基本類型中的string與復雜類型中的String的區別。

也就是說,string類型都是放在棧內存中的(類似於常量),如果string類型的變量的值改為另一個string,那么棧內存中原有的string並沒有變化,只不過是在棧內存中新開辟出一個string,然后改變變量的引用而已
而Strign的型的棧內存中只有指針,指向堆內存的數據。所以如果值進行了改變,是會直接修改堆內存中的數據內容,而棧內存中的指針並不會變。
function類型與object類型的區別
這個一個知識點也是很多人疑惑的地方,明明只有一種復雜對象Object,但為什么一些函數類型的typeof 返回function,其它對象返回 object呢?
- 簡單點可以這樣理解:Object,Function,String等都是JavaScript內置的函數對象,typeof是獲取函數對象的類型,所以返回fucntion。而new Object()、new String()等是構造出一個新的對象,所以typeof返回object。而new Fucntion()構造處理的仍然是一個函數對象,所以返回fucntion
Function是最頂層的構造器。它構造了系統中所有的對象,包括用戶自定義對象,系統內置對象,甚至包括它自已。
- 關於Object和Function可以這樣理解
null是天地之始,然后null生了Object,Object是萬物之母。然后Object有一個屬性constructor,恰巧可以返回function的值,所以typeof Object為function。然后Function是基於Object派生的。Function.prototype._proto_指向Object.prototype。(Function.constructor也返回function值)
- 如果要深入理解,需要對JS中的原型和原型鏈有一定了解
參考 JS原型和原型鏈的理解
- 示例
function a(){}; var b = function(){}; var c = new Function(); var d = new Object(); var e = new String(); var f = new Date(); console.log(typeof a);//function console.log(typeof b);//function console.log(typeof c);//function console.log(typeof Function);//function console.log(typeof Object);//function console.log(typeof d);//object console.log(typeof String);//function console.log(typeof e);//object console.log(typeof Date);//function console.log(typeof f);//object console.log(Object instanceof Function);//true console.log(Function instanceof Object);//true console.log(new Object() instanceof Object);//true console.log(new Object() instanceof Function);//false console.log(new Function() instanceof Function);//true console.log(new Function() instanceof Object);//true function Foo(){}; var foo = new Foo(); console.log(foo instanceof Foo);//true console.log(foo instanceof Function);//false console.log(foo instanceof Object);//true console.log(Foo instanceof Function);//true console.log(Foo instanceof Object);//true
==和===的區別
==和===在JS中都有比較的意思,但是兩者有着很大的不同,兩者區別如下:
- 對於string,number,boolean等基礎簡單類型而言,==和===是有區別的
因為不同類型的值比較,==會將比較值轉換為同一類型的值后 在看值是否相等。===的話會先判斷類型,如果類型不同,結果就是不等。
- 對於引用類型而言,==和===是沒有區別的
因為這類值得比較都是“指針地址”比較,不同的值,肯定為false
- 對於基礎類型和引用類型比較而言,==和===是有區別的
對於==會將復雜類型轉換為基礎類型,進行值比較,對於===,由於類型不同,直接為false
- 示例
var a = 1; var b = true; console.log(a == b); //true,轉換為同一類型后值相等 console.log(a === b); //false,先比較類型不能,直接為false var a = { 'test': '1' }; var b = { 'test': '1' }; console.log(a == b); //false,比較指針地址,不等 console.log(a === b); //false,比較指針地址,不等 var a = '11'; var b = new String('11'); console.log(a == b); //true,將高級類型String轉化為基礎類型,值相等 console.log(a === b); //false,因為類型不同,直接為false
typeof的作用
JS的變量的值是松散類型的(弱類型),可以保存任何類型的數據,JS內置的typeof可以檢查給定變量的數據類型,可能的返回值如下:
- undefined:undefined類型
- boolean:boolean類型
- string:string類型
- number:number類型
- object:null類型或者其它的引用型(object,去除function)
- function:這個值是函數對象,引用類型中的特殊類型(可以認為引用型中除去了function就是object)
console.log(typeof 'test'); //'string'
console.log(typeof 101); //'number'
console.log(typeof true); //'boolean'
console.log(typeof undefined); //'undefined'
console.log(typeof null); //'object'
console.log(typeof function() {}); //'function'
console.log(typeof {}); //object
console.log(typeof new Date()); //object
instanceof的作用
instanceof用於判斷一個變量是否是某個對象的實例,主要是判斷某個構造函數的prototype屬性是否存在另一個要檢查對象的原型鏈上。
- Instanceof可以判斷內置的對象類型(基於Obejct對象拓展的,如Array,Date等)。
- 可以判斷自定義對象類型,如下述中的Child和Parent
- 但是不能判斷簡單類型(因為本質是通過原型來判斷,但是簡單類型只是一個常量,並不是引用對象)
//識別內置對象 - Array, Date等
console.log([] instanceof Array); //true
console.log(new String('11') instanceof String); //true
console.log('11'
instanceof String); //false,因為11是簡單類型
//識別自定義對象類型以及父子類型
function Parent(x) {
this.x = x;
}
function Child(x, y) {
Parent.call(this, x);
this.y = y;
}
//將Child的原型指向Parent,表明繼承關系,此時Child的構造變為了Parent的構造
Child.prototype = new Parent();
//然后將構造函數換為Child自己的
Child.prototype.constructor = Child;
console.log(Child.prototype.constructor); //輸出構造函數是Child自己的
var person = new Child(1, 2);
console.log(person.x + ',' + person.y); //1,2
console.log(person instanceof Child); //true
console.log(person instanceof Parent); //true
//不能識別簡單類型,因為instanceof后面只能是基於Object對象拓展的類型
console.log(101 instanceof number); //報錯,number is not defined
Object.prototype.toString的作用
Object.prototype.toString的存在主要是為了解決typeof和instanceof的不足,比如typeof無法識別內置類型(Array Date等),而instanceof無法識別簡單類型。所以才有了這個。
Object.prototype.toString可以識別5種簡單類型,以及全部內置類型(Array.Date等一些內置類型),但是無法識別自定義對象類型
/**
* @description 通過Object.prototype.toString來判斷傳入對象類別
* @param {Object} obj
*/
function type(obj) {
//slice的作用,例如本來返回[object number],slice篩選出number
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
console.log(type(1)); //number
console.log(type('1')); //string
console.log(type(true)); //boolean
console.log(type(undefined)); //undefined
console.log(type(null)); //null
console.log(type(new Date())); //date
console.log(type([])); //array
console.log(type({})); //object
function Test(a) {
this.a = a;
}
console.log(type(new Test('1'))); //object,自定義類別只能識別為object
基本數據類型為什么能夠使用toString等操作
前面說到JS中有基本類型和引用類型(對象類型、復雜類型各種說法都行)。請注意兩者是有本質區別的。
- 基本類型的值進行toString操作時,並不是用自身進行操作的,而是有類似與“裝箱”、“拆箱”的操作
var a = 10.1; console.log(a.toFixed(2));//10.10,臨時構建了個Number對象進行操作,操作完后銷毀了 a.foo = 'test'; console.log(a.foo); // undefined,因為a的值並不是一個對象,無法綁定屬性
上述代碼中,對基本類型的
a進行a.xx操作時,會在內部臨時創建一個對應的包裝類型(比如number類型對應Number類型)的臨時對象。並把對基本類型的操作代理到對這個臨時對象身上,使得對基本類型的屬性訪問看起來像對象一樣。但是在操作完成后,臨時對象就扔掉了,下次再訪問時,會重新建立臨時對象,當然對之前的臨時對象的修改都不會有效了。
JavaScript築基篇(三)->JS原型和原型鏈的理解
說明
JS中原型和原型鏈是很重要的知識點,本文內容則是我對於它的理解。建議讀本文時已經有了一點的JS基礎。
目錄
前言
參考來源
前人栽樹,后台乘涼,本文參考了以下來源
- 湯姆大叔:強大的原型和原型鏈
- JS原型對象與原型鏈
- Js中Prototype、__proto__、Constructor、Object、Function關系介紹
- 關於JS中的constructor與prototype
- SF:JS中先有函數還是先有對象
前置技術要求
閱讀本文前,建議先閱讀以下文章
楔子
學習是有瓶頸的,JS學習也同樣,基本上JS的學習中,到了原型與原型鏈這一步,就會遇到瓶頸,只有真正理解它,才能跨過去,進入新的領域
曾經看過很多網上關於JS原型的介紹,基本上每完整的看完一篇,就會“噢,恍然大悟的樣子”。然后仔細想想,發現自己並沒有真正的理解。所以這里將自己對應原型的理解寫下來,希望能幫助他人快速理解原型。
起由
簡單的描述下,原型是什么,如何誕生的。
null開天辟地
"無,名天地之始;有,名萬物之母" -引自 《道德經》
- (無)在JS中,本來也是什么都沒有的,只有一個 null
- (道生一)然后出現了Object.prototype對象
- (一生二)然后基於Object.prototype產生出了Function.prototype對象
注意,JS中所有對象都不是函數構造出來的,對象是由"JavaScript運行時環境"以原型對象為模板,直接產生出來的,構造函數只是以新生的對象為this,做一些初始化操作。(-引自參考來源)
- (二生三)然后基於兩個prototype,產生出了兩個構造器Object和Function。這時候出現了函數對象的概念,實例對象,原型對象的概念
- Object,Function都屬於函數對象
- new Object()、new Function()出來的是實例對象
所以,后面我們一般會認為實例對象是由函數對象構造出來的
- Object.prototype,Function.prototype是原型,原型對象的作用就是以它為模板產生其它對象,它也和普通實例對象一樣,擁有constructor,__proto__屬性
原型對象之所以要有constructor,__proto__屬性,可以認為是在產生實例對象時,方便實例對象指向具體的構造函數以及完成一個原型鏈的作用。
- (三生萬物)然后基於前面的Object,Object.prototype,Function,Function.prototype,產生出了其它各種對象
可以先簡單的理解以上步驟為JS對象的誕生過程(排除基本型的值,因為它們並不屬於對象-沒有對象的這些特性)
當然,里面具體Object.prototype、Object、Function.prototype這些內置對象的產生過程是很復雜的,上述只是為了便於理解的一種簡單粗暴的概念。
前因后果
函數對象、實例對象與原型對象
再開始理解原型之前我們得先明確一個概念:"函數對象","實例對象","原型對象"
- 原型對象是對象的原型。原型對象有constructor,__proto__屬性
可以認為所有對象都是原型產生的(萬物之母可以認為是Object.prototype)。
- 函數對象有 prototype,__proto__屬性
如JS內置的Object,Function對象都是函數對象。其中prototype的值就就是它對應的原型對象
- 實例對象有 constructor,__proto__屬性
如new Object(),new Function()出來的都是實例對象。其中constructor指向它的構造函數,__proto__指向產生它的原型
- 關系如圖
constructor、__proto__與prototype
在我們有了上述概念后,再來分析JS中的constructor,__proto__與prototype
//Object,Function都是函數對象
var 實例對象 = new 函數對象();
//Person也是函數對象
function Person(name) {
this.name = name;
this.say = function() {
console.log(this.name);
}
};
var one = new Person('test');
console.log(Person.prototype);//{constructor:Person函數,__proto__:Object.prototype}
console.log(Person.prototype.constructor === Person);//true
console.log(Person.prototype.__proto__ === Object.prototype);//true
console.log(Person.prototype.__proto__ === Function.prototype);//false
console.log(Person.__proto__.__proto__ === Object.prototype);//true
console.log(Person.__proto__.constructor === Function);//true
console.log(Person.__proto__.constructor.prototype === Object.__proto__);//true
console.log(Person.__proto__.constructor.__proto__ === Object.__proto__);//true
console.log(Object.__proto__ === Person.__proto__);//true
console.log(Person.__proto__.constructor.prototype === Person.__proto__);//true,相當於自己構建了自己...
console.log(one.prototype); //undefined
console.log(one.constructor === Person); //true
console.log(one.__proto__ === Person.prototype); //true
console.log(Object.prototype.constructor);//Object函數:function Object() { [native code] }
console.log(Function.prototype.__proto__ === Object.prototype);//true
console.log(Function.prototype.constructor === Function);//true
console.log(Object.__proto__);//Function.prototype
console.log(Function.prototype);//Function.prototype
console.log(Function.__proto__);//Function.prototype
console.log(Object.prototype.__proto__);//undefined
console.log(Function.prototype === Function.__proto__);//true
console.log(Object.__proto__ === Function.__proto__);//true
console.log(Object.__proto__ === Function.prototype);//true
console.log(Function.prototype.constructor === Function);//true
console.log(Function.__proto__.constructor === Function);//true
console.log(Object.__proto__ === undefined);//false
console.log(Object.prototype.__proto__ === undefined);//true,原型鏈的盡頭為null
var two = {};
console.log(two.constructor === Object);//true
console.log(two.prototype);//undefined
console.log(two.__proto__ === Object.prototype);//true
如上代碼所示,有如下總結
- 函數對象Person有一個prototype屬性,有一個__proto__屬性。prototype屬性的值是一個prototype對象。prototype有一個constructor屬性(為了方便稱為 $constructor),有一個__proto__屬性(為了方便稱為 $__proto__)。
$constructor屬性的值是一個constructor對象。而這個constructor對象恰恰就是這個函數對象本身(Person本身)。
$__proto__屬性的值是Object.prototype(相當於就是函數對象Object的prototype屬性)原型鏈就是基於__proto__字段不斷往上找,直到遇到null為止
__proto__屬性的值是Function.prototype,Function.prototype和其它原型對象一樣,有一個__proto__屬性和constructor屬性
- __proto__屬性的值是Object.prototype
- constructor的值是一個constructor對象。這個對象即為Function
- Function對象和Object對象一樣,也有它的prototype和__proto__。Function的prototype與__proto__的值都是Function.prototype
注意,__proto__屬性的名稱並不規范,如Chrome中叫__proto__,但IE中不一定叫這個名字,但是我們一般習慣把它叫成__proto__(但注意是__proto__並不是_proto_)
- 實例對象one沒有prototype屬性(所以one.prototype===undefined)。示例對象one有一個constructor屬性(為了方便稱為 $constructor),有一個__proto__屬性(為了方便稱為 $__proto__)。
$constructor屬性的值是一個constructor對象。而這個constructor對象恰恰就是這個函數對象本身(Person本身)。
$__proto__屬性的值是Person.prototype(相當於就是函數對象Person的prototype屬性)所以現在就構成了一個原型鏈 one.__proto__ ->Person.prototype;Person.prototype.__proto__->Object.prototype;Object.prototype.__proto__ ->null
-
如上圖中,可以清晰的看到Person函數對象的實例one是由Person構造的,所以one.constructor===Person。同樣,普通的object對象two是由Object構造的,所以two.constructor===Object
由此可以看出,Function對象 原型鏈上有Function.prototype和Object.prototype,所以 Function是Function類型的,也是Object類型的。另外Object對象的原型鏈上有Function.prototype和Object.prototype,所以Object是Function類型的,也是Object類型的
從上,我們還可以得到一個概念: "Object.prototype是所有對象的原始原型對象(所有對象的原型鏈最終都會指向它);Function.prototype是所有函數對象的原始原型對象(所有函數對象的原型鏈最終都會指向它),而Function.prototype的原型鏈指向Object.prototype"
注意,Object,Function對象的產生是很復雜的,里面甚至涉及到了自己構建自己,這里只是簡化版本,詳情請參考MDN...
原型與原型鏈
區分原型對象與原型鏈
原型對象
- 如前面提到的JS內置對象Object.prototype,Function.prototype等就是原型對象,原型對象的作用是可以以它為原型產生其它對象。每一個原型對象都有一個隱藏的屬性(如在chrome中是__proto__,不同瀏覽器實現不同),這個屬性的值是另一個原型對象
原型鏈
- 原型鏈是一個概念
- 每一個JS的實例對象都有一個__ptoto__屬性,這個屬性指向產生它的原型對象,然后就像前面提到的,每一個原型對象也有一個__proto__屬性,指向與產生它的原型
- 就這樣,從實例->原型1->...->原始原型(Object.prototype)->null。這樣就組成了一條鏈,這個就是原型鏈
- JavaScritp引擎在訪問對象的屬性時,如果在對象本身中沒有找到,則會去原型鏈中查找,如果找到,直接返回值,如果整個鏈都遍歷且沒有找到屬性,則返回undefined
原型鏈的作用
原型鏈的一個最大的作用就是,可以基於它,實現JS中的繼承。
因為JS在ES6之前,是沒有Class這個概念的,只能通過原型鏈來進行實現。
原型鏈的流程與示例
原型鏈的原理就是基於__proto__屬性不斷的往上查找,下面介紹下一些原型鏈的用法示例
示例一
var base = {
name: 'base',
say: function(){
console.log(this.name);
}
};
var one = {
name: 'one',
__proto__:base
};
var two = {
__proto__:base
};
console.log(one.name);//one
one.say();//one
console.log(two.name);//base
two.say();//base
代碼分析:
- 以上代碼中的base是由Object.prototype產生的,所以base.__proto__的值為Object.prototype
- one和two原本也是由Object.prototype產生的,所以本來__proto__也是指向Object.prototype的,但是這里手動修改了這個指向,變為指向base了
- 所以就有了兩個原型鏈:(one -> base ->Object.prototype),(two -> base -> Object.prototype)
- 然后根據原形鏈的規則,現在本對象上找屬性,沒有的話再根據原形鏈指向一層一層往上找,直到找到null返回undefined為止。
所以才會有以上的輸出結果。one.name是one自身的屬性,one.say()是上一級原型鏈base的屬性,two.name,say()都是上一級base的屬性。
- 可以總結為如圖所示(去除__ptoto__之外的干擾因素)
示例二
function Base(name){
this.sex = 0;
this.name = name || 'base';
this.hello = function(){
console.log("hello " + name);
};
}
Base.prototype.say = function(){
console.log('name:'+this.name);
};
function Extend(name,num){
//讓Base能初始化屬性
Base.call(this,name);
this.num = num || 0;
}
//注意,這里是new Base()而不是Base.prototype
//因為new Base()是新建了一個對象,這樣可以不會影響到Base.prototype
//否則如果直接操作Base.prototype,會污染Base.prototype
Extend.prototype = new Base();
//前面由於將prototype變為了new Base()所以構造方法默認是Base的
//這里需要手動替換回來
Extend.prototype.constructor = Extend;
var one = new Extend('one',2);
console.log(Extend.__proto__);
console.log(one instanceof Extend);//true
console.log(one instanceof Base);//true
console.log(one.constructor === Extend);//true
console.log(one.__proto__ === Extend.prototype);//true
console.log(one.name);//one
console.log(one.sex);//0
console.log(one.num);//2
one.say();//name:one
one.hello();//hello one
代碼分析:
- 上述代碼在進行原型鏈修改前,有如下原型鏈
一條鏈為:new Extend() -> Extend.prototype ->Object.prototype -> null
一條鏈為: new Base() -> Base.prototype ->Object.prototype -> null
一條鏈為: Extend -> Function.prototype ->Object.prototype -> null
一條鏈為: Base -> Function.prototype ->Object.prototype -> null
- 修改原型后,有了一條完整的繼承鏈(針對於實例對象而言,相當於上述的第一條拓展了)
new Extend() -> Extend.prototype -> Base.prototype ->Object.prototype -> null
- 而根據原形鏈,所以上述的代碼會有這些輸出
one是Extend的實例對象,所以one自身找不到時,會沿着原型鏈往上找,知道原型鏈的盡頭都沒有找到,則返回null
- 可以總結為如圖所示(去除干擾因素)

