當你使用new
的時候,會:
- 建立一個新的空物件;
- 將
this
繫結到該物件;- 新增一個名為
__proto__
的新屬性,並且指向建構函式的原型(prototype);- 返回該
this
物件。
執行new命令時的原理步驟:
- 建立一個空物件,作為將要返回的物件例項
- 將這個空物件的原型,指向建構函式的
prototype
屬性- 將這個空物件賦值給函式內部的
this
關鍵字- 開始執行建構函式內部的程式碼
如果你沒有特別理解,那麼我們接下來用例子來詳細解釋。首先定義一個建構函式Student
,該函式接收兩個引數name
和age
。
function Student(name, age){
this.name = name;
this.age = age;
}複製程式碼
現在我們使用new
來建立一個新的物件:
var first = new Student('John', 26);複製程式碼
到底發生了什麼呢?
- 一個新的物件建立,我們叫它
obj
; this
繫結到obj
,任何對this
的引用就是對obj
的引用;__proto__
屬性被新增到obj
物件。obj.__proto__
會指向Student.prototype
;- 該
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命令時的原理步驟:
- 建立一個空物件,作為將要返回的物件例項
- 將這個空物件的原型,指向建構函式的
prototype
屬性 - 將這個空物件賦值給函式內部的
this
關鍵字 - 開始執行建構函式內部的程式碼
注意點:當建構函式裡面有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.....
*/複製程式碼