1. 程式人生 > >10段程式碼打通js學習的任督二脈

10段程式碼打通js學習的任督二脈

簡單回撥程式碼

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    fn();
}
function doFoo2(o){
    o.foo();
}
var obj = {
    a: 2,
    foo: foo
};
var a = "I'm an a";
doFoo(obj.foo);
doFoo2(obj);

分析

在Javascript中,this指向函式 執行時的當前物件,而非宣告環境有
執行doFoo的時候執行環境就是doFoo函式,執行環境為全域性。
執行doFoo2時是在物件內部呼叫函式,this指標指向該物件。

結果

I'm an a
2

用apply改變函式作用域

function foo(somthing){
    console.log(this.a, somthing);
}
function bind(fn, obj){
    return function(){
        return fn.apply(obj, arguments);
    }
}
var obj = {
    a:2
}
var bar = bind(foo, obj);
var b = bar(3);
console.log(b);

分析

apply、call、bind都有個作用就是改變作用域,這裡用apply將foo函式的作用域指向obj物件,同時傳入引數。
再簡單分析一下bind函式內部的巢狀,執行bind函式的時候返回的是一個匿名函式,所以執行bar(3)的時候實際上是執行的bind內部的匿名函式,返回的是之前傳入的foo函式的執行結果。
函式沒有返回值的情況下預設返回undefined。

結果

2 3
undefined

new關鍵字

function foo(a,b){
    this.val = a+b;
}
var bar = foo.bind(null, 'p1');
var baz = new bar('p2');
console.log(baz.val);

分析

bind函式的第一個引數為null代表作用域不變,後面的不定引數將會和函式本身的引數按次序進行繫結,繫結之後執行函式只能從未繫結的引數開始傳值。

此處的this指向新建立的物件,之所以要在new中使用硬繫結函式,主要目的是預先設定函式的一些引數,這樣在使用new進行初始化時就可以只傳入其餘的引數。bind(...)的功能之一就是可以把除了第一個引數(第一個引數用於繫結this)之外的其他引數都傳給下層的函式(這種技術稱為“部分應用”,是“柯里化”的一種)

結果

p1p2

自執行函式

function foo(){
    console.log(this.a);
}
var a = 2;
var o = {a:3,foo:foo};
var p = {a:4};
(p.foo=o.foo)();

分析

經常可以看到這樣的程式碼

(function(){
    //...
})()

這種程式碼通常是建立一個立即執行的函式同時避免汙染全域性變數。
很少有人去關注賦值語句執行之後會返回什麼結果,其實就是返回當前值。也就是說當括號內執行完賦值之後,返回的是o物件中的foo函式。函式的執行環境中有一個a物件,嗯,就是它了~

答案

2

變數屬性

var a = [];
a[0] = 1;
a['foobar'] = 2;
console.log(a.length);
console.log(a.foobar);

分析

當一個變數被聲明後,擴充其屬性並不會改變原資料型別。

結果

1
2

精度問題

var a = 'foo';
a[1] = 'O';
console.log(0.1+0.2==0.3||a);

分析

當操作小數時請小心,js的小數計算並不精確,所以上面的判斷是false。
字串變數是常量。

結果

foo

命名提升

foo();
var foo = 0;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
};

分析

宣告的變數和命名函式都會被提升到程式碼的最前面,只不過宣告的變數的賦值語句在程式碼中的位置不變。所以上面這段程式碼應該被理解為:

var foo;
function foo(){
    console.log(1);
}
foo();
foo = 0;
foo = function(){
    console.log(2);
};

結果

1

思考

foo();
var foo = 0;
function foo(){
    console.log(1);
}
foo();
foo = function(){
    console.log(2);
};
foo();

上面程式碼的結果:

1
報錯

作用域

foo();
var a = true;
if(a){
    function foo(){
        console.log('a');
    }
} else {
    function foo(){
        console.log('b');
    }
}

分析

javascript並不是以程式碼段為作用域,而是以函式。
再根據命名提升的原則,所以這段程式碼應該是這樣的:

function foo(){
    console.log('a');
}
function foo(){
    console.log('b');
}
foo();
var a = true;
if(a){
} else {
}

結果

b

閉包陷阱

for(var i=1;i<=5;i++){
    setTimeout(function(){
        console.log(i);
    }, i*1000);
}

分析

閉包有個重要的作用就是,在內層函式引用外層函式定義的變數時,外層函式的變數不會被會被持久化。
這裡有個隱藏陷阱就是for迴圈結束之後i仍然自增了1。

結果

6
6
6
6
6

偽閉包

function foo(){
    console.log(a);
}
function bar () {
    var a = 3;
    foo();
}
var a = 2;
bar();

分析

閉包是函式的巢狀定義,而不是函式的巢狀呼叫。

結果

2

思考

如何輸出3?

function bar () {
    function foo(){
        console.log(a);
    }
    var a = 3;
    foo();
}
var a = 2;
bar();

彩蛋

光說不練假把式~
一週月內將下題正確答案發送至我郵箱內(郵箱地址請參考部落格),將獲得本年度我閱讀過最優秀的關於AngularJS的電子書一本。

var Obj = {
    name: 'zdl',
    do: function(){
        console.log(this.name);
    }
}

寫個物件a繼承Obj的方法(不使用new)。

 

 

 

 

 

我們所需要做的就是找到函式的呼叫位置並判斷應用哪條規則,不過,如果某個呼叫位置可以應用多條規則該怎麼辦?為了解決這個問題就必須給這些規則設定優先順序,對於正常的函式呼叫來說,優先順序為 new繫結 > 顯示繫結 > 隱式繫結 > 預設繫結,所以我們可以按照下面的順序來進行判斷:

  1. 函式是否在new中呼叫(new 繫結) ? 如果是的話this繫結的是新建立的物件。
    var bar = new foo() //此處foo函式裡面的this指向bar
    注: 可以在new中使用硬繫結函式,如下
function foo(p1,p2){
    this.val = p1 + p2;
}
var bar = foo.bind(null,'p1')
var baz = new bar('p2')
console.log(baz.val);//p1p2

此處的this指向新建立的物件,之所以要在new中使用硬繫結函式,主要目的是預先設定函式的一些引數,這樣在使用new進行初始化時就可以只傳入其餘的引數。bind(...)的功能之一就是可以把除了第一個引數(第一個引數用於繫結this)之外的其他引數都傳給下層的函式(這種技術稱為“部分應用”,是“柯里化”的一種)

  1. 函式是否通過call、apply(顯示繫結)或者bind(硬繫結)呼叫?如果是的話,this繫結的是指定的物件
    var bar = foo.call(obj2) //此處foo函式裡的this指向obj2
  2. 函式是否在某個上下文物件中呼叫(隱式繫結)?如果是的話,this繫結的是那個上下文物件(注意有隱式丟失的情況)
    var bar=obj1.foo() //此處this指向obj1
  3. 如果都不是的話,使用預設繫結,嚴格模式下this繫結到undefined,非嚴格模式下繫結到全域性物件
    var bar = foo() //此處foo函式裡面的this指向全域性物件

但是在某些場景下this的繫結行為會出乎意料,你認為應當應用其他繫結規則時,實際上應用的可能是預設繫結規則,這被稱為繫結例外。

被忽略的this

  • 當把null或者undefined作為this的繫結物件傳入call、apply或者bind時,,這些值在呼叫的時候會被忽略,實際應用的是預設繫結規則:
function foo(){
    console.log(this.a);
}
var a = 2;
foo.call(null); //2   所以此處foo函式裡的this指向全域性物件

一般在什麼樣的情況下我們會傳入null呢,比如某個函式接收多個引數,但此時我們想傳入的卻是一個數組,便可以使用apply(...)來接收這個引數陣列,類似地,使用bind(...)可以對引數進行柯里化(預先設定一些引數)

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}
//通過使用apply傳入陣列
foo.apply(null,[2,3]);//a:2,b:3

//使用bind進行柯里化
var bar = foo.bind(null,1);
bar(2);//a:1,b:2

這兩種方法都需要傳入一個引數當作this的繫結物件。如果函式內部根本不關心this的話,我們仍需要傳入一個佔位符,這時null便成了一個不錯的選擇,在ES6中,可以用...擴充套件運算子代替上面的apply方法展開陣列,這樣可以避免不必要的this繫結,但仍然沒有柯里化的相關語法,因此還是需要使用bind(...)
  然而,總是使用null來忽略this繫結可能產生一些副作用,如果某個函式內部確實使用了this(比如第三方庫中的一個函式),那預設繫結規則會把this繫結到全域性物件(在瀏覽器中這個物件是window),這將導致無法預料的後果(比如修改了全域性物件的屬性),顯而易見,這種方式可能會導致許多難以分析和追蹤的bug.
  一種“更安全”的做法是傳入一個特殊的物件,把this繫結到這個物件不會對我們的程式產生任何副作用。就像網路(以及軍隊)一樣,我們可以建立一個“DMZ“(demilitarized zone,非軍事區)物件--它就是一個空的非委託物件(何為非委託物件?後續繼續瞭解).如果我們在忽略this繫結時總是傳入一個DMZ物件,那就什麼都不用擔心了,因為任何對於this的使用都會被限制在這個空物件中,不會對全域性物件產生任何影響。一般可使用∅變數名來表示這個空物件。在JavaScript中建立一個物件最簡單的方法是Object.create(null)。它和{}很像,但是並不會建立Object.prototype這個委託,所以它比{}“更空”:

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}
const ∅=Object.create(null);
foo.apply(∅,[2,3]);  //a:2,b:3
const bar=foo.bind(∅,2);
bar(3);  //a:2,b:3

通過使用∅這個空物件不僅讓函式變得更加“安全”,而且可以提高程式碼的可讀性。

  • 另一種this被忽略的情況發生在隱式繫結被間接引用時,此時,會應用預設繫結規則。
    常見的間接引用有兩種情況:
    賦值:將某個隱式繫結的函式控制代碼賦值給一個變數,然後通過這個變數執行函式,這便屬於間接引用,函式裡的this將應用預設繫結規則,不再指向隱式繫結的那個物件
    當作引數傳入函式時,一般作用回撥函式使用,其實傳參相當於一種隱式賦值,和上面一樣,應用預設繫結規則。
    注意:對於預設繫結來說,決定this繫結物件的並不是呼叫位置是否處於嚴格模式,而是函式體是否處於嚴格模式。如果函式體處於嚴格模式,this會被繫結到undefined,否則this會被繫結到全域性物件。

軟繫結

之前我們已經看到過,硬繫結這種方式可以把this強制繫結到指定的物件(除了使用new時),防止函式呼叫應用預設繫結規則。問題在於,硬繫結會大大降低函式的靈活性,使用硬繫結之後就無法使用隱式繫結或者顯式繫結來修改this。
如果可以給預設繫結指定一個全域性物件和undefined以外的值,那就可以實現和硬繫結相同的效果,同時保留隱式繫結或者顯式繫結修改this的能力。
可以通過一種被稱為軟繫結的方法來實現我們想要的效果:

if(!Function.prototype.softBind){
    Function.prototype.softBind=function(obj){
        const fn = this;//這裡的this取決於softBind函式的呼叫位置
        const carried = Array.prototype.slice.call(arguments,1);
        const bind = function(){
            return fn.apply(
                (!this||this===window)?obj:this,//這裡的this由軟繫結後的函式呼叫位置決定,注意,與上面的this不同
                Array.prototype.concat.apply(carried,arguments)
           );
        }
        bind.prototype = Object.create(fn.prototype);
        return bind;
    }
}

除了軟繫結之外,softBind(...)的其他原理和ES5內建的bind(...)類似,它會對指定的函式進行封裝,首先檢查呼叫時的this,如果this繫結到全域性物件或者undefined,那就把指定的預設物件obj繫結到this,否則不修改this。此外,這段程式碼還支援可選的柯里化。

function foo(){
    console.log("name:"+this.name);
}
const obj1={name:"obj1"},obj2={name:"obj2"},obj3={name:"obj3"};
const fooObj=foo.softBind(obj1);
fooObj();//name:"obj1"  軟繫結
obj2.foo=foo.softBind(obj1);
obj2.foo();//name:"obj2"  隱式繫結修改了this
fooObj.call(obj3);//name:"obj3"  顯示繫結修改了this

setTimeout(obj2.foo,100);//name:"obj1"  當應用預設繫結時,則會應用我們自定義的軟繫結(在node環境下顯示undefined,不知此處為何依然應用了預設繫結)

注意:箭頭函式

在ES6中定義了一種新的函式型別:箭頭函式,箭頭函式並不是使用function關鍵字定義的,而是使用被稱為“胖箭頭”的操作符=>定義的,箭頭函式本身取消了this機制,使用更常見的詞法作用域替代this,箭頭函式裡的this繼承自外層函式的this。箭頭函式的繫結無法被修改,new也不行。

以上內容來自簡書與慕課。