“new” 都做了些啥以及this繫結?

weixin_33686714發表於2017-10-21

當你使用new的時候,會:

  1. 建立一個新的空物件;
  2. this繫結到該物件;
  3. 新增一個名為__proto__的新屬性,並且指向建構函式的原型(prototype);
  4. 返回該this物件。


執行new命令時的原理步驟:

  1. 建立一個空物件,作為將要返回的物件例項
  2. 將這個空物件的原型,指向建構函式的prototype屬性
  3. 將這個空物件賦值給函式內部的this關鍵字
  4. 開始執行建構函式內部的程式碼

如果你沒有特別理解,那麼我們接下來用例子來詳細解釋。首先定義一個建構函式Student,該函式接收兩個引數nameage

 function Student(name, age){
  this.name = name;
  this.age = age;
}複製程式碼

現在我們使用new來建立一個新的物件:

  var first = new Student('John', 26);複製程式碼

到底發生了什麼呢?

  1. 一個新的物件建立,我們叫它obj
  2. this繫結到obj,任何對this的引用就是對obj的引用;
  3. __proto__屬性被新增到obj物件。obj.__proto__會指向Student.prototype
  4. obj物件被賦值給first變數。

我們可以通過列印測試:

  console.log(first.name);
// John

console.log(first.age);
// 26複製程式碼

接下來深入看看__proto__是怎麼回事。

原型(Prototype)

每一個JavaScript物件都有一個原型。所有的物件都從它的原型中繼承物件和屬性。

開啟瀏覽器開發者控制皮膚(Windows: Ctrl + Shift + J)(Mac: Cmd + Option + J),輸入之前定義的Student函式:

  function Student(name, age) {
  this.name = name;
  this.age = age;
}複製程式碼

為了證實每一個物件都有原型,輸入:

  Student.prototype;
// Object {...}複製程式碼

你會看到返回了一個物件。現在我們來嘗試定義一個新的物件:

  var second = new Student('Jeff', 50);複製程式碼

根據之前的解釋,second指向的物件會有一個__proto__屬性,並且應該指向父親的prototype,我們來測試一下:

 second.__proto__ === Student.prototype
// true複製程式碼

Student.prototype.constructor會指向Student的建構函式,我們列印出來看看:

  Student.prototype.constructor;
//  function Student(name, age) {
//    this.name = name;
//    this.age = age;
//  }複製程式碼

好像事情越來越複雜了,我們用圖來形象描述一下:

Student的建構函式有一個叫.prototype的屬性,該屬性又有一個.constructor的屬性反過來指向Student構造。它們構成了一個環。當我們使用new去建立一個新的物件,每一個物件都有.__proto__屬性反過來指向Student.prototype

這個設計對於繼承來說很重要。因為原型物件被所有由該建構函式建立的物件共享。當我們新增函式和屬性到原型物件中,其它所有的物件都可以使用。

在本文我們只建立了兩個Student物件,如果我們建立20,000個,那麼將屬性和函式放到prototype而不是每一個物件將會節省非常很多的儲存和計算資源。

我們來看一個例子:

  Student.prototype.sayInfo = function(){
  console.log(this.name + ' is ' + this.age + ' years old');
}複製程式碼

我們為Student的原型新增了一個新的函式sayInfo – 所以使用Student建立的學生物件都可以訪問該函式。

  second.sayInfo();
// Jeff is 50 years old複製程式碼

建立一個新的學生物件,再次測試:

var third = new Student('Tracy', 15);// 如果我們現在列印third, 雖然只會看到年齡和名字這兩個屬性,// 仍然可以訪問sayInfo函式。

var third = new Student('Tracy', 15);
// 如果我們現在列印third, 雖然只會看到年齡和名字這兩個屬性,
// 仍然可以訪問sayInfo函式。
third;
// Student {name: "Tracy", age: 15}
third.sayInfo();
// Tracy is 15 years old
複製程式碼

在JavaScript中,首先檢視當前物件是否擁有該屬性;如果沒有,看原型中是否有該屬性。這個規則會一直持續,直到成功找到該屬性或則到最頂層全域性物件也沒找到而返回失敗。

繼承讓你平時不需要去定義toString()函式而可以直接使用。因為toString()這個函式內建在Object的原型上。每一個我們建立的物件最終都指向Object.prototype,所以可以呼叫toString()。當然, 我們也可以重寫這個函式:

  var name = {
  toString: function(){
    console.log('Not a good idea');
  }
};
name.toString();
// Not a good idea複製程式碼

建立的name物件首先檢視是否擁有toString,如果有就不會去原型中查詢。









一、建構函式和new命令

1、建構函式

  • JavaScript語言的物件體系,不是基於“類”的,而是基於建構函式(constructor)和原型鏈(prototype)
  • 為了與普通函式區別,建構函式名字的第一個字母通常大寫,比如: var Person = function(){ this.name = '王大錘'; }
  • 建構函式的特點:
    a、函式體內部使用了this關鍵字,代表了所要生成的物件例項;
       b、生成物件的時候,必需用new命令呼叫此建構函式

2、new

  作用:就是執行建構函式,返回一個例項物件 

var Person = function(name, age){
    this.name = name;
    this.age = age;
    this.email = 'cnblogs@sina.com';
    this.eat = function(){
        console.log(this.name + ' is eating noodles');
    }
}

var per = new Person('王大錘', 18);
console.log(per.name + ', ' + per.age + ', ' + per.email); //王大錘, 18, cnblogs@sina.com
per.eat();  //王大錘 is eating noodles複製程式碼

執行new命令時的原理步驟:

  1. 建立一個空物件,作為將要返回的物件例項
  2. 將這個空物件的原型,指向建構函式的prototype屬性
  3. 將這個空物件賦值給函式內部的this關鍵字
  4. 開始執行建構函式內部的程式碼

注意點:當建構函式裡面有return關鍵字時,如果返回的是非物件,new命令會忽略返回的資訊,最後返回時構造之後的this物件;
  如果return返回的是與this無關的新物件,則最後new命令會返回新物件,而不是this物件。示例程式碼:

console.log('---- 返回字串 start ----');
var Person = function(){
    this.name = '王大錘';
    return '羅小虎';
}

var per = new Person();
for (var item in per){
    console.log( item + ': ' + per[item] );
}
//---- 返回字串 start ----
//name: 王大錘

console.log('----- 返回物件 start ----');
var PersonTwo = function(){
    this.name = '倚天劍';
    return {nickname: '屠龍刀', price: 9999 };
}
var per2 = new PersonTwo();
for (var item in per2){
    console.log(item + ': ' + per2[item]);
}
//----- 返回物件 start ----
//nickname: 屠龍刀
//price: 9999複製程式碼

如果呼叫建構函式的時候,忘記使用new關鍵字,則建構函式裡面的this為全域性物件window,屬性也會變成全域性屬性,

則被建構函式賦值的變數不再是一個物件,而是一個未定義的變數,js不允許給undefined新增屬性,所以呼叫undefined的屬性會報錯。

示例:

var Person = function(){ 
    console.log( this == window );  //true
    this.price = 5188; 
}
var per = Person();
console.log(price); //5188
console.log(per);  //undefined
console.log('......_-_'); //......_-_
console.log(per.price); //Uncaught TypeError: Cannot read property 'helloPrice' of undefined複製程式碼

為了規避忘記new關鍵字現象,有一種解決方式,就是在函式內部第一行加上 : 'use strict';

表示函式使用嚴格模式,函式內部的this不能指向全域性物件window, 預設為undefined, 導致不加new呼叫會報錯

var Person = function(){ 
    'use strict';
    console.log( this );  //undefined
    this.price = 5188; //Uncaught TypeError: Cannot set property 'helloPrice' of undefined
}

var per = Person(); 複製程式碼

另外一種解決方式,就是在函式內部手動新增new命令:

var Person = function(){ 
    //先判斷this是否為Person的例項物件,不是就new一個
    if (!(this instanceof Person)){
        return new Person();
    }
    console.log( this );  //Person {}
    this.price = 5188; 
}

var per = Person(); 
console.log(per.price); //5188複製程式碼

二、this關鍵字

var Person = function(){
    console.log('1111'); 
    console.log(this); 
    this.name = '王大錘';
    this.age = 18;

    this.run = function(){
        console.log('this is Person的例項物件嗎:' + (this instanceof Person) ); 
        console.log(this); 
    }
}

var per = new Person();
per.run();
/* 列印日誌:
1111
Person {}
this is Person的例項物件嗎:true
Person {name: "王大錘", age: 18, run: function}
*/

console.log('---------------');

var Employ = {
    email: 'cnblogs@sina.com',
    name: '趙日天',
    eat: function(){
        console.log(this);
    }
}

console.log(Employ.email + ', ' + Employ.name);
Employ.eat();
/* 列印日誌:
---------------
cnblogs@sina.com, 趙日天
Object {email: "cnblogs@sina.com", name: "趙日天", eat: function}
*/複製程式碼

1、this總是返回一個物件,返回屬性或方法當前所在的物件, 如上示例程式碼

2、物件的屬性可以賦值給另一個物件,即屬性所在的當前物件可變化,this的指向可變化

var A = { 
    name: '王大錘', 
    getInfo: function(){
        return '姓名:' + this.name;
    } 
}

var B = { name: '趙日天' };

B.getInfo = A.getInfo;
console.log( B.getInfo() ); //姓名:趙日天

//A.getInfo屬性賦給B, 於是B.getInfo就表示getInfo方法所在的當前物件是B, 所以這時的this.name就指向B.name複製程式碼

3、由於this指向的可變化性,在層級比較多的函式中需要注意使用this。一般來說,在多層函式中需要使用this時,設定一個變數來固定this的值,然後在內層函式中這個變數。

示例1:多層中的this

//1、多層中的this (錯誤演示)
var o = {
    f1: function(){
        console.log(this); //這個this指的是o物件

        var f2 = function(){
            console.log(this);
        }();
        //由於寫法是(function(){ })() 格式, 則f2中的this指的是頂層物件window
    }
}

o.f1();
/* 列印日誌:
Object {f1: function}

Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
*/


//2、上面程式碼的另一種寫法(相同效果)
var temp = function(){
    console.log(this);
}
var o = {
    f1: function(){
        console.log(this); //這個this指o物件
        var f2 = temp(); //temp()中的this指向頂層物件window
    }
}
o.f1(); 
/* 列印日誌
Object {f1: function}

Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
*/
//表示上面兩種寫法是一樣的效果,this的錯誤演示


//3、多層中this的正確使用:使用一個變數來固定this物件,然後在內層中呼叫該變數
var o = {
    f1: function(){
        console.log(this); //o物件
        var that = this;
        var f2 = function(){
            console.log(that); //這個that指向o物件
        }();
    }
}
o.f1();
/* 列印日誌:
Object {f1: function}
Object {f1: function}
*/複製程式碼

示例2: 陣列遍歷中的this

//1、多層中陣列遍歷中this的使用 (錯誤演示)
var obj = {
    email: '大錘@sina.com', 
    arr: ['aaa', 'bbb', '333'],
    fun: function(){
        //第一個this指的是obj物件
        this.arr.forEach(function(item){
            //這個this指的是頂層物件window, 由於window沒有email變數,則為undefined
            console.log(this.email + ': ' + item);
        });
    }
}

obj.fun(); 
/* 列印結果:
undefined: aaa
undefined: bbb
undefined: 333
 */

//2、多層中陣列遍歷中this的使用 (正確演示,第一種寫法)
var obj = {
    email: '大錘@sina.com', 
    arr: ['aaa', 'bbb', '333'],
    fun: function(){
        //第一個this指的是obj物件
        var that = this; //將this用變數固定下來
        this.arr.forEach(function(item){
            //這個that指的是物件obj
            console.log(that.email + ': ' + item);
        });
    }
}
obj.fun(); //呼叫
/* 列印日誌:
大錘@sina.com: aaa
大錘@sina.com: bbb
大錘@sina.com: 333
 */


//3、多層中陣列遍歷中this正確使用第二種寫法:將this作為forEach方法的第二個引數,固定迴圈中的執行環境
var obj = {
    email: '大錘@sina.com', 
    arr: ['aaa', 'bbb', '333'],
    fun: function(){
        //第一個this指的是obj物件
        this.arr.forEach(function(item){
            //這個this從來自引數this, 指向obj物件
            console.log(this.email + ': ' + item);
        }, this);
    }
}
obj.fun(); //呼叫
/* 列印日誌:
大錘@sina.com: aaa
大錘@sina.com: bbb
大錘@sina.com: 333
 */


//4.箭頭函式

var obj = {
    email: '大錘@sina.com', 
    arr: ['aaa', 'bbb', '333'],
    fun: function(){
        //第一個this指的是obj物件
        this.arr.forEach((item)=>{
            //這個this從來自引數this, 指向obj物件
            console.log(this.email + ': ' + item);
        });
    }
}
obj.fun(); //呼叫
VM83:8 大錘@sina.com: aaa
VM83:8 大錘@sina.com: bbb
VM83:8 大錘@sina.com: 333



//5. 使用bind

var obj = {
    email: '大錘@sina.com', 
    arr: ['aaa', 'bbb', '333'],
    fun: function(){
        //第一個this指的是obj物件
        this.arr.forEach(function(item){
            //這個this從來自引數this, 指向obj物件
            console.log(this.email + ': ' + item);
        }.bind(this));
    }
}
obj.fun(); //呼叫
VM122:8 大錘@sina.com: aaa
VM122:8 大錘@sina.com: bbb
VM122:8 大錘@sina.com: 333
undefined複製程式碼

4、關於js提供的call、apply、bind方法對this的固定和切換的用法

1)、function.prototype.call(): 函式例項的call方法,可以指定函式內部this的指向(即函式執行時所在的作用域),然後在所指定的作用域中,呼叫該函式。
  如果call(args)裡面的引數不傳,或者為null、undefined、window, 則預設傳入全域性頂級物件window;
  如果call裡面的引數傳入自定義物件obj, 則函式內部的this指向自定義物件obj, 在obj作用域中執行該函式

var obj = {};
var f = function(){
    console.log(this);
    return this;
}

console.log('....start.....');
f();
f.call();
f.call(null);
f.call(undefined);
f.call(window);
console.log('**** call方法的引數如果為空、null和undefined, 則預設傳入全域性等級window;如果call方法傳入自定義物件obj,則函式f會在物件obj的作用域中執行 ****');
f.call(obj);
console.log('.....end.....');

/* 列印日誌:
....start.....
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
**** call方法的引數如果為空、null和undefined, 則預設傳入全域性等級window;如果call方法傳入自定義物件obj,則函式f會在物件obj的作用域中執行 ****
Object {}
.....end.....
*/複製程式碼


相關文章