前端常見知識點
1.基本資料型別
答:js有六大資料型別,其中包括五種基本資料型別和一種複雜型別。如下:
五種基本資料型別:undefined null Boolean Number String
複雜型別為:Object
ES6新出的型別:Symbol
symbol:
1)http://es6.ruanyifeng.com/#docs/symbol
2)用於宣告屬性名是獨一無二的,故任何兩個相同的屬性名無論是否值相同都是不能等同的。還有,symbol是型別,而不是物件,不能通過new例項化。
常見注意點:
1)undefined為變數已宣告但未定義
2)null表示空物件指標,如:
var car=null; console.log(typeof car );//null
3)undefined是派生自null的,所以
undefined==null;//true
4)對於Boolean
var foo;
alert(!foo);//undifined情況下,一個感嘆號返回的是true;
alert(!goo);//null情況下,一個感嘆號返回的也是true;
var o={flag:true};
var test=!!o.flag;//等效於var test=o.flag||false;
alert(test);
5)Number中
alert(NaN == NaN); //false
2,原型prototype _proto_ 和原型鏈
1)prototype和_proto_的區別
var a = {};
console.log(a.prototype); //undefined
console.log(a.__proto__); //Object {}
var b = function(){}
console.log(b.prototype); //b {}
console.log(b.__proto__); //function() {}
2)_proto_指向誰
_proto_的指向取決於物件建立時的實現方式。以下圖示列出了三種常見方式建立物件後,_proto_分別指向誰
/*1、字面量方式*/
var a = {};
console.log(a.__proto__); //Object {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*2、構造器方式*/
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*3、Object.create()方式*/
var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object {a: 1}
console.log(a.__proto__ === a.constructor.prototype); //false(此處即為圖1中的例外情況)
3)原型鏈
由於_proto_是任何物件都有的屬性,而js萬物皆有物件,所以會形成一條_proto_連起來的鏈條,遞迴訪問_proto_必須最終到頭,並且值為null
當js引擎找物件的屬性時,先查詢物件本身是否存在該屬性,如果不存在,會在原型鏈上查詢,但不會查詢自身的prototype
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}(即構造器function A 的原型物件)
console.log(a.__proto__.__proto__); //Object {}(即構造器function Object 的原型物件)
console.log(a.__proto__.__proto__.__proto__); //null
3.閉包
作用域
先來談談變數的作用域
變數的作用域無非就是兩種:全域性變數和區域性變數。
全域性作用域:
最外層函式定義的變數擁有全域性作用域,即對任何內部函式來說,都是可以訪問的:
<script>
var outerVar = "outer";
function fn(){
console.log(outerVar);
}
fn();//result:outer
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
區域性作用域:
和全域性作用域相反,區域性作用域一般只在固定的程式碼片段內可訪問到,而對於函式外部是無法訪問的,最常見的例如函式內部
<script>
function fn(){
var innerVar = "inner";
}
fn();
console.log(innerVar);// ReferenceError: innerVar is not defined
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
需要注意的是,函式內部宣告變數的時候,一定要使用var命令。如果不用的話,你實際上聲明瞭一個全域性變數!
<script>
function fn(){
innerVar = "inner";
}
fn();
console.log(innerVar);// result:inner
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
再來看一個程式碼:
<script>
var scope = "global";
function fn(){
console.log(scope);//result:undefined
var scope = "local";
console.log(scope);//result:local;
}
fn();
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
很有趣吧,第一個輸出居然是undefined,原本以為它會訪問外部的全域性變數(scope=”global”),但是並沒有。這可以算是javascript的一個特點,只要函式內定義了一個區域性變數,函式在解析的時候都會將這個變數“提前宣告”:
<script>
var scope = "global";
function fn(){
var scope;//提前聲明瞭區域性變數
console.log(scope);//result:undefined
scope = "local";
console.log(scope);//result:local;
}
fn();
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
然而,也不能因此草率地將區域性作用域定義為:用var宣告的變數作用範圍起止於花括號之間。
javascript並沒有塊級作用域
那什麼是塊級作用域?
像在C/C++中,花括號內中的每一段程式碼都具有各自的作用域,而且變數在宣告它們的程式碼段之外是不可見的,比如下面的c語言程式碼:
for(int i = 0; i < 10; i++){
//i的作用範圍只在這個for迴圈
}
printf("%d",&i);//error
- 1
- 2
- 3
- 4
但是javascript不同,並沒有所謂的塊級作用域,javascript的作用域是相對函式而言的,可以稱為函式作用域:
<script>
for(var i = 1; i < 10; i++){
//coding
}
console.log(i); //10
</script>
- 1
- 2
- 3
- 4
- 5
- 6
作用域鏈(Scope Chain)
那什麼是作用域鏈?
我的理解就是,根據在內部函式可以訪問外部函式變數的這種機制,用鏈式查詢決定哪些資料能被內部函式訪問。
想要知道js怎麼鏈式查詢,就得先了解js的執行環境
執行環境(execution context)
每個函式執行時都會產生一個執行環境,而這個執行環境怎麼表示呢?js為每一個執行環境關聯了一個變數物件。環境中定義的所有變數和函式都儲存在這個物件中。
全域性執行環境是最外圍的執行環境,全域性執行環境被認為是window物件,因此所有的全域性變數和函式都作為window物件的屬性和方法建立的。
js的執行順序是根據函式的呼叫來決定的,當一個函式被呼叫時,該函式環境的變數物件就被壓入一個環境棧中。而在函式執行之後,棧將該函式的變數物件彈出,把控制權交給之前的執行環境變數物件。
舉個例子:
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面程式碼執行情況演示:
瞭解了環境變數,再詳細講講作用域鏈。
當某個函式第一次被呼叫時,就會建立一個執行環境(execution context)以及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性([scope])。然後使用this,arguments(arguments在全域性環境中不存在)和其他命名引數的值來初始化函式的活動物件(activation object)。當前執行環境的變數物件始終在作用域鏈的第0位。
以上面的程式碼為例,當第一次呼叫fn1()時的作用域鏈如下圖所示:
(因為fn2()還沒有被呼叫,所以沒有fn2的執行環境)
可以看到fn1活動物件裡並沒有scope變數,於是沿著作用域鏈(scope chain)向後尋找,結果在全域性變數物件裡找到了scope,所以就返回全域性變數物件裡的scope值。
識別符號解析是沿著作用域鏈一級一級地搜尋識別符號地過程。搜尋過程始終從作用域鏈地前端開始,然後逐級向後回溯,直到找到識別符號為止(如果找不到識別符號,通常會導致錯誤發生)—-《JavaScript高階程式設計》
那作用域鏈地作用僅僅只是為了搜尋識別符號嗎?
再來看一段程式碼:
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
outer()內部返回了一個inner函式,當呼叫outer時,inner函式的作用域鏈就已經被初始化了(複製父函式的作用域鏈,再在前端插入自己的活動物件),具體如下圖:
一般來說,當某個環境中的所有程式碼執行完畢後,該環境被銷燬(彈出環境棧),儲存在其中的所有變數和函式也隨之銷燬(全域性執行環境變數直到應用程式退出,如網頁關閉才會被銷燬)
但是像上面那種有內部函式的又有所不同,當outer()函式執行結束,執行環境被銷燬,但是其關聯的活動物件並沒有隨之銷燬,而是一直存在於記憶體中,因為該活動物件被其內部函式的作用域鏈所引用。
具體如下圖:
outer執行結束,內部函式開始被呼叫
outer執行環境等待被回收,outer的作用域鏈對全域性變數物件和outer的活動物件引用都斷了
像上面這種內部函式的作用域鏈仍然保持著對父函式活動物件的引用,就是閉包(closure)
閉包
閉包有兩個作用:
第一個就是可以讀取自身函式外部的變數(沿著作用域鏈尋找)
第二個就是讓這些外部變數始終儲存在記憶體中
關於第二點,來看一下以下的程式碼:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的區域性變數
result[i] = function(){
return i;
}
}
return result;//返回一個函式物件陣列
//這個時候會初始化result.length個關於內部函式的作用域鏈
}
var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
返回結果很出乎意料吧,你肯定以為依次返回0,1,但事實並非如此
來看一下呼叫fn[0]()
的作用域鏈圖:
可以看到result[0]函式的活動物件裡並沒有定義i這個變數,於是沿著作用域鏈去找i變數,結果在父函式outer的活動物件裡找到變數i(值為2),而這個變數i是父函式執行結束後將最終值儲存在記憶體裡的結果。
由此也可以得出,js函式內的變數值不是在編譯的時候就確定的,而是等在執行時期再去尋找的。
那怎麼才能讓result陣列函式返回我們所期望的值呢?
看一下result的活動物件裡有一個arguments,arguments物件是一個引數的集合,是用來儲存物件的。
那麼我們就可以把i當成引數傳進去,這樣一呼叫函式生成的活動物件內的arguments就有當前i的副本。
改進之後:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個帶參函式
function arg(num){
return num;
}
//把i當成引數傳進去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]);//result:0
console.log(fn[1]);//result:1
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
雖然的到了期望的結果,但是又有人問這算閉包嗎?呼叫內部函式的時候,父函式的環境變數還沒被銷燬呢,而且result返回的是一個整型陣列,而不是一個函式陣列!
確實如此,那就讓arg(num)函式內部再定義一個內部函式就好了:
這樣result返回的其實是innerarg()函式
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個帶參函式
function arg(num){
function innerarg(){
return num;
}
return innerarg;
}
//把i當成引數傳進去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]());
console.log(fn[1]());
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
當呼叫outer,for迴圈內i=0時的作用域鏈圖如下:
由上圖可知,當呼叫innerarg()時,它會沿作用域鏈找到父函式arg()活動物件裡的arguments引數num=0.
上面程式碼中,函式arg在outer函式內預先被呼叫執行了,對於這種方法,js有一種簡潔的寫法
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個帶參函式
result[i] = function(num){
function innerarg(){
return num;
}
return innerarg;
}(i);//預先執行函式寫法
//把i當成引數傳進去
}
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
關於this物件
關於閉包經常會看到這麼一道題:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());//result:The Window
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
《javascript高階程式設計》一書給出的解釋是:
this物件是在執行時基於函式的執行環境繫結的:在全域性函式中,this等於window,而當函式被作為某個物件呼叫時,this等於那個物件。不過,匿名函式具有全域性性,因此this物件同常指向window
4,js中this指向問題
js中this指向問題老生常談的問題了,在這裡說一下我所理解的層面的this。
this 指的是當前物件,如果在全域性範圍內使用this,則指代當前頁面window;如果在函式中使用this,則this指代什麼是根據當前函式是在什麼物件上呼叫。我們可以使用call和apply改變函式中this的具體指向。
console.log(this === window) // true console.log(window.alert === this.alert) // true console.log(this.parseInt("021",10)) // 21
parseInt(string,radix); // 當只使用一個引數時,我們都知道是將其轉為整數; // radix 取值為 2~36,表示當前數是幾進位制,並將這個數轉成十進位制,當不在這個範圍時,會輸出NaN;
函式中的this是在執行時候決定的,而不是函式定義時。
function foo(){ console.log(this.fruit); } // 定義一個全域性變數,等同於window.fruit = "banana"; var fruit = "banana"; // 此時函式中的this指向window; foo(); // "banana" var o = { fruit : "apple", foo : foo }; // 此時函式中的this指向o o.foo(); // "apple"
全域性函式apply和call可以用來改變this的指向,如下:
function foo(){ console.log(this.fruit); } // 定義一個全域性變數,等同於window.fruit = "banana"; var fruit = "banana"; var o = { fruit : "apple" }; foo.apply(window); // "banana"; foo.call(o); // "apple";
apply和call的唯一區別,就是在傳參的時候,apply的引數需要放在一個數組裡面,而call不需要;
因為在JavaScript中,函式也是物件,我們看下面這個例子:
1 function foo(){ 2 if(this === window){ 3 console.log("this is window"); 4 } 5 }; 6 // 函式foo也是物件,可以為物件定義屬性,然後屬性為函式 7 foo.boo = function(){ 8 if(this === foo){ 9 console.log("this is foo"); 10 }else if(this === window){ 11 console.log("this is window"); 12 } 13 }; 14 15 // 等價於 window.foo(); 16 foo(); // "this is window"; 17 // 可以看到函式中this的指向呼叫函式的物件 18 foo.boo(); // "this is foo"; 19 // 可以使用call改變函式中this指向 20 foo.boo.call(window); // "this is window";
物件中的巢狀函式的this指向不是當前物件,而是window,看如下例子:
var name = "window.name"; var obj = { name : "obj.name", getName:function(){ console.log(this.name); return function(){ console.log(this.name); } } } obj.getName()(); // "obj.name" "window.name"
同樣是obj呼叫的getName和getName裡面的方法,結果卻是不同的值,這就說明巢狀函式中的this已經不指向當前物件了,而指向window。
那麼,我們要怎樣解決上述問題呢?主要有三種解決辦法,如下:
1.使用函式的bind方法,綁定當前this;
1 var name = "window.name"; 2 var obj = { 3 name : "obj.name", 4 getName:function(){ 5 console.log(this.name); 6 return function(){ 7 console.log(this.name); 8 }.bind(this); 9 } 10 }; 11 obj.getName()(); // "obj.name" "obj.name"
2.使用變數將上面的this接收一下,然後下面不使用this,使用那個變數;
1 var name = "window.name"; 2 var that = null; 3 var obj = { 4 name : "obj.name", 5 getName:function(){ 6 that = this; 7 console.log(this.name); 8 return function(){ 9 console.log(that.name); 10 } 11 } 12 } 13 obj.getName()(); // "obj.name" "obj.name"
3.使用ES6的箭頭函式,可以完美避免此問題;
1 var name = "window.name"; 2 var obj = { 3 name : "obj.name", 4 getName:function(){ 5 console.log(this.name); 6 return () => { 7 console.log(this.name); 8 } 9 } 10 } 11 obj.getName()(); // "obj.name" "obj.name"