本系列將從如下專題去總結:javascript
1. JS基礎知識深刻總結
2. 對象高級
3. 函數高級
4. 事件對象與事件機制html
暫時會對以上四個專題去總結,如今開始Part3:函數高級。下圖是我這篇的大綱。 java
this
是在函數執行的過程當中自動建立的一個指向一個對象的內部指針。確切的說,this
並非一個對象,而是指向一個已經存在的對象的指針,也能夠認爲是this
就是存儲了某個對象的地址。面試
this
的指向不是固定的,會根據調用的不一樣,而指向不一樣的地方。 此章節的講解思路是這樣的: chrome
對在全局做用域中定義的變量和函數的進一步認識編程
永遠記住:只要是在全局做用域聲明的任何變量和函數默認都是做爲window對象的屬性而存在的.json
理解完以上的這句話,咱們在來講明一下其中的區別,這是不少人沒有關注過的。數組
console.log(window.a); //undefined
console.log(a); //報錯!a is not defined
複製代碼
註解:也就是在未對變量(a
)進行聲明時,就會出現以上結果。首先明確一點,就是全局變量a
是window
的屬性。因此,咱們從這裏就能夠發現,何時undefined
,何時報錯?——那就是若是是訪問一個對象的屬性時,它沒有聲明賦值,那就是undefined
;若是訪問一個變量,它沒有聲明賦值,那就是報錯。promise
好,如今回頭過來,咱們看全局做用域變量和函數的認識。瀏覽器
<script type="text/javascript">
var num = 10; //全局做用域聲明的變量
function sum () { //全局做用域聲明的函數
alert("我是一個函數");
}
alert(window.num); // 10
window.sum(); // 我是一個函數
// 在調用的時候window對象是能夠省略的。
</script>
複製代碼
構造函數和非構造函數的澄清
在JavaScript中構造函數和非構造函數沒有本質的區別。惟一的區別只是調用方式的區別。
看一個示例代碼:
<script type="text/javascript">
function Person () {
this.age = 20;
this.sex = "男";
}
//做爲構造函數調用,建立一個對象。 這個時候實際上是給p添加了兩個屬性
var p = new Person();
alert(p.age + " " + p.sex);
//做爲普通函數傳遞,實際上是給 window對象添加了兩個屬性
//任何函數本質上都是經過某個對象來調用的,若是沒有直接指定就是window,也就是Window可省略
Person();
alert(window.age + " " + window.sex);
</script>
複製代碼
全局做用域中使用
this
,也就是說不在任何的函數內部使用this
,那麼這個時候this
就是指的 Window
<script type="text/javascript">
//全局做用域中的this
//向this對象指代的對象中添加一個屬性 num, 並讓屬性的值爲100
this.num = 100;
// 由於this就是window,因此這時是在修改屬性num的值爲200
window.num = 200;
alert(this === window); // true this就是指向的window對象,因此是恆等
alert(this.num); //200
alert(window.num); //200
</script>
複製代碼
函數中this
又可分爲構造函數和非構造函數的this
兩個概念去理解。
非構造函數中的this
指向
非構造函數中
this
指向的就是 調用這個方法的那個對象
示例1:
<script type="text/javascript">
function test() {
alert(this == window);
this.age = 20;
}
test(); //實際上是 window.test(); 因此這個時候test中的this指向window
</script>
複製代碼
示例2:
<script type="text/javascript">
var p = {
age : 20,
sex : "男",
sayAge: function (argument) {
alert(this.age);
}
}
p.sayAge(); //調用對象p的方法sayAge() 因此這個時候this指的是 p 這個對象
</script>
複製代碼
示例3:
<script type="text/javascript">
var p = {
age : 20,
sex : "男",
sayAge: function (argument) {
alert(this.age);
alert(this === p); //true
}
}
var again = p.sayAge; //聲明一個變量(方法),把p的方法複製給新的變量
//調用新的方法: 實際上是window.again().
//因此 方法中的this指代的是window對象,這個時候age屬性是undefined
// this和p也是不相等的。
again();
</script>
複製代碼
綜上:
this
的指代和代碼出現的位置無關,只和調用這個方法的對象有關。
構造方法中的
this
指代的要將來要建立的那個對象。
示例1:
<script type="text/javascript">
function Person () {
this.age = 20;
return this; //做爲構造函數的時候,這個行代碼默認會添加
}
var p1 = new Person(); //這個時候 Person中的this就是指的p1
var p2 = new Person(); //這是時候 Person中的this就是知道p2
</script>
複製代碼
多瞭解一點:其實用
new
調用構造函數的時候,構造函數內部其實有個默認的return this
; 這就是爲何this
指代那個要建立的對象了。
在JavaScript中,容許更改this
的指向。 經過call
方法或apply
方法
函數A能夠成爲指定任意對象的方法進行調用 。函數A就是函數對象,每一個函數對象中都有一個方法call
,經過call
可讓你指定的對象去調用這個函數A。
ECMAScript 規範給全部函數都定義了call
與 apply
兩個方法。 call
和apply
是放在Function的原型對象上的,而不是Object原型對象上!
<script type="text/javascript">
var age = 20;
function showPropertyValue (propertyName) {
alert(this[propertyName]);
}
//使用call的時候,第一個參數表示showPropertyValue中的this的執行,後面的參數爲向這個函數傳的值。
//注意一點:若是第一個參數是null,則this仍然是默認的指向。
showPropertyValue.call(null, "age");
showPropertyValue.call(this, "age");
showPropertyValue.call({age:50}, "age")
</script>
複製代碼
在this
的指向中有第三個方向就是經過call
/apply
去改變this
的指向,這個JavaScript中一個獨特的使用形式,其餘語言並無。那麼,咱們就在這裏順帶講一下call
、apply
以及bind
的用法。
本小節將從三個方面講解: 1:apply
和call
的區別 2:apply
和call
的用法 3:call
和bind
的區別
ECMAScript 規範給全部函數都定義了 call
與apply
兩個方法,它們的應用很是普遍,它們的做用也是如出一轍,只是傳參的形式有區別而已。
簡單來講,假設有一個函數A,咱們調用函數A會直接去A()
,那麼若是是A()
這樣直接調用的話,函數體A裏面的this
就是window
了。而咱們能夠經過call
(或apply
)去調用,好比:A.call()
.這樣子調用就能夠指定A中的this究竟是哪一個對象。
用call
來作比對,裏面有兩個參數,參數一就是從新指定其中的this
是誰,參數2是屬性名。而事實上,call
與apply
也就是參數二的不一樣這個差別。
apply
apply
方法傳入兩個參數:一個是做爲函數上下文的對象,簡單來講,從新指定函數中的this
是誰。另一個是做爲函數參數所組成的數組,是傳入一個數組。
var obj = {
name : 'ya LV'
}
function func(firstName, lastName){
console.log(firstName + ' ' + this.name + ' ' + lastName);
}
func.apply(obj, ['A', 'B']); // A ya LV B
複製代碼
能夠看到,obj
是做爲函數上下文的對象,也就是說函數func
中 this
指向了 obj
這個對象。原本若是直接調用func()
,那麼函數體中的this
就是指的是window
。可是如今有了參數一,就是從新指定this
,這個this
就是參數一的obj
這個對象。參數 A 和 B 是放在數組中傳入 func
函數,分別對應 func
參數的列表元素。
call
call
方法第一個參數也是做爲函數上下文的對象。與apply
沒有任何區別。可是後面傳入的是一個參數列表,而不是單個數組。
var obj = {
name: 'ya LV'
}
function func(firstName, lastName) {
console.log(firstName + ' ' + this.name + ' ' + lastName);
}
func.call(obj, 'C', 'D'); // C ya LV D
複製代碼
對比apply
咱們能夠看到區別,C 和 D 是做爲單獨的參數傳給 func
函數,而不是放到數組中。
對於何時該用什麼方法,其實不用糾結。若是你的參數原本就存在一個數組中,那天然就用apply
,若是參數比較散亂相互之間沒什麼關聯,就用 call
。
補充一個使用
apply
的例子。好比求一個數組的最大值?
明確JavaScript中沒有返回一個數組中最大值的函數。可是,有一個函數Math.max
能夠返回任意多個數值類型的
參數中的最大值,Math.max
函數入參並不支持數組,只能是將多個參數逐個傳入,用逗號分隔。
這個時候若是要能夠用Math.max函數,且傳參數能夠是一個數組。咱們天然而然會想到全部函數都定義了call
和apply
的方法,咱們能夠配合apply
或call
來實現,又由於call
傳參數並非一個數組。全部咱們就選擇出Math.max
函數加上apply
就能夠實現咱們的目的。
本來只能這樣用,並不能直接用數組,見示例:
let max = Math.max(1, 4, 8, 9, 0)
複製代碼
有了 apply
,就能夠這麼調用:
let arr = [1, 4, 8, 9, 0];
let max = Math.max.apply(null, arr);
複製代碼
在調用apply
的時候第一個參數給了一個null
,這個是由於沒有對象去調用這個方法,咱們只須要用這個方法幫咱們運算,獲得返回的結果就行,因此就直接傳遞了一個null
過去。
apply
和call
的用法能夠分爲三個:改變this
的指向,借用別的對象的方法,調用函數。
var obj = {
name: 'ya LV'
}
function func() {
console.log(this.name);
}
func.call(obj); // ya LV
複製代碼
這個在前一小節有講到,因此咱們就簡單的再來看看。所謂「熟能生巧」,同樣東西,一個知識點,每看一次會有不一樣的體會,可能此次看的過程讓你有更深入的思考,這就是進步。call
方法的第一個參數是做爲函數上下文的對象,這裏把 obj
做爲參數傳給了func
,此時函數裏的this
便指向了obj
對象。此處func
函數裏其實至關於:
function func() {
console.log(obj.name);
}
複製代碼
另外注意下call
的一些特別用法,很奇葩的this
指向。稍微注意下,有點印象就好。
function func() {
console.log(this);
}
func.call(); //window
func.call(undefined); //window
func.call(null); //window
func.call(1); //Number {1} 這種狀況會自動轉換爲包裝類Number 就至關於下面一行代碼
func.call(new Number(1)); //Number {1}
複製代碼
var Person1 = function () {
this.name = 'ya LV';
}
var Person2 = function () {
this.getname = function () {
console.log(this.name);
}
Person1.call(this);
}
var person = new Person2();
person.getname(); // ya LV
複製代碼
從上面咱們看到,Person2
實例化出來的對象 person
經過 getname
方法拿到了 Person1
中的 name
。由於在 Person2
中,Person1.call(this)
的做用就是使用Person1
對象代替 this
對象,那麼 Person2
就有了Person1
中的全部屬性和方法了,至關於 Person2
繼承了Person1
的屬性和方法。 不理解的話咱們再來慢慢看,咱們說A.call ( 參數一)
這樣的形式就是從新指定函數A
中的this
是‘參數一’
這個對象,那麼咱們來看看Person2
函數體中的Person1.call(this)
這條語句,其中這條語句的this
是指Person2
這個對象。如今就是把Person1
函數的this
從新指向爲Person2
,是否是有了Person2.name='ya LV'
。
apply
、call
方法都會使函數當即執行,所以它們也能夠用來調用函數。這個咱們在這節的一開始就有說,好比A()
和A.call()
都是調用函數A。
function func() {
console.log('ya LV');
}
func.call(); // ya LV
複製代碼
在 EcmaScript5
中擴展了叫 bind
的方法,在低版本的 IE 中不兼容。它和call
很類似,接受的參數有兩部 分,第一個參數是是做爲函數上下文的對象,第二部分參數是個列表,能夠接受多個參數。
它們之間的區別有如下兩點。
var name='HELLO'
var obj = {
name: 'ya LV'
}
function func() {
console.log(this.name);
}
//將func的代碼拷貝一份,而且永遠改變其拷貝出來的函數中的this,爲bind第一個參數所指向的對象。把這 份永遠改變着this指向的函數返回給func1.
var func1 = func.bind(obj);
//bind方法不會當即執行,是返回一個改變上下文this的函數,要對這個函數調用纔會執行。
func1(); //ya LV
//能夠看到,如今這份改變this以後拷貝過來的函數,this的指向永遠是bind()綁定的那個,無論以後去call 從新指向對象,func1 都不會改變this的指向。永遠!可知,bind比call優先級還高。
func1.call({name:'CALL'}); //ya LV
//又從func從新拷貝一份永遠改變this指向對象爲{name:'LI SI'}這個對象的函數,返回給func2.
var func2 = func.bind({name:'LI SI'});
func2(); //LI SI
//注意,這裏是拷貝一份func2(而不是func)的代碼,而func2以前已經綁定過去永遠改變this的指向了,因此這 裏並不去改變!仍是會輸出原來的最早bind的this指向對象。
var func3 = func2.bind({name:'ZHANG SAN'});
func3(); //LI SI
//上面對func最初的函數進行了屢次綁定,綁定後原函數 func 中的 this 並無被改變,依舊指向全局對象 window。由於綁定bind的過程是拷貝代碼的一個過程,而不是在其自身上修改。window.name = HELLO
func(); //HELLO
複製代碼
bind
方法不會當即執行,而是返回一個改變了上下文this
後的函數。而原函數func
中的 this
並無被改變,依舊指向全局對象 window
。
function func(a, b, c) {
console.log(a, b, c);
}
var func1 = func.bind(null,'yaLV');
func('A', 'B', 'C'); // A B C
func1('A', 'B', 'C'); // yaLV A B
func1('B', 'C'); // yaLV B C
func.call(null, 'yaLV'); // yaLV undefined undefined
複製代碼
call
是把第二個及之後的參數做爲 func
方法的實參傳進去,而 func1
方法的實參實則是在bind
中參數的基礎上再日後排。也就是說,var func1 = func.bind(null,'yaLV');
bind
現有兩個參數,第一個是指向,第二個實參是'yaLV'
,那麼就是先讓func
中的a='yaLV'
,而後沒排滿就是讓func1('A', 'B', 'C')
; 這個參數依次排,如今b='A'
,c='B'
, 形參已經排完了。也就是輸出yaLV A B
。
在低版本瀏覽器沒有bind
方法,咱們也能夠本身實現一個。
if (!Function.prototype.bind) {
Function.prototype.bind = function () {
var self = this, // 保存原函數
context = [].shift.call(arguments), // 保存須要綁定的this上下文
args = [].slice.call(arguments); // 剩餘的參數轉爲數組
return function () { // 返回一個新函數
self.apply(context[].concat.call(args[].slice.call(arguments));
}
}
}
複製代碼
習題1
<script type="text/javascript">
var name='window_dqs';
var obj={
name:'obj_dqs',
showName:function(){
console.log(this.name);
}};
function fn(){
console.log(this);
}
function fn2(){
this.name='fn_dqs';
}
//由於obj去調用,this就是obj
obj.showName(); //obj_dqs
//由於借調,而此時借調的對象是this,而this在全局做用域上就是指window,因此找window.name
obj.showName.apply(this); //window_dqs
//由於借調的對象是一個函數對象,那麼this就是指函數對象,this.name就是函數名
obj.showName.apply(fn2); //fn2
</script>
複製代碼
習題2
<script type="text/javascript">
var name='window_dqs';
function fn(){
this.name='fn_dqs';
this.showName=function(){
console.log(this.name);
}
console.log(this);
}
function fn2(){
this.name='fn_pps';
this.showName=function(){
console.log(this.name);
}
console.log(this);
}
var p=new fn();
fn2.apply(p);
p.showName();
var obj={};
fn2.apply(obj);
obj.showName();
</script>
複製代碼
結果是:
var p=new fn();輸出fn { name: 'fn_dqs', showName: [Function] }
fn2.apply(p);輸出fn { name: 'fn_pps', showName: [Function] }
p.showName();輸出fn_pps
var obj={};
fn2.apply(obj);輸出Object{name: "fn_pps"showName: ƒ ()__proto__: Object··}
obj.showName();輸出fn_pps
複製代碼
習題3
<script type="text/javascript">
var name='window_dqs';
var obj={
name:'json_dqs',
showName:function(){
console.log(this.name);
return function(){
console.log(this.name);
}
}
}
var p=obj.showName();
obj.showName()();
p.call(obj);
</script>
複製代碼
結果是:
json_dqs
json_dqs
window_dqs
json_dqs
複製代碼
面試題1
代碼片斷1:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function (){
return function (){
return this.name;
};
}
};
console.log(object.getNameFunc()); //ƒ (){return this.name;}
console.log(object.getNameFunc()()); //The Window
複製代碼
代碼片斷一沒有閉包。有嵌套,但沒有用外部函數的變量或函數。是使用this
的。this
與調用方式有關。
理解:看object.getNameFunc()
是對象.方法() 返回的是一個函數,這個函數還未執行。js中this
是動態的,因此函數沒有執行,並不肯定函數裏的this
是指的是誰?那麼如今再對返回的函數加個(),也就是object.getNameFunc()()
,調用執行,把最後一個括號和最後一個括號前當作兩個部分,前面是函數名,後面一個括號是調用。至關於test()
,這個時候this
就是window
。故這樣調用的函數this就是指的window
,故window.name=The Window
.
代碼片斷二: 對於片斷一咱們的本意是否是想輸出My Object
。那麼怎麼改造,經過that=this
去操做。
var name2 = "The Window";
var object2 = {
name2: "My Object",
getNameFunc: function () {
var that = this; //緩存this
return function () {
return that.name2;
};
}
};
console.log(object2.getNameFunc()); //ƒ (){return that.name2;}
console.log(object2.getNameFunc()()); //My Object
複製代碼
代碼片斷二是有閉包的,有嵌套函數。內部函數有使用外部函數的變量that
。外部和內部函數有執行。
理解:首先仍是看object2.getNameFunc()
返回一個函數,注意這個函數中沒有this
,在調用object2.getNameFunc
時,咱們有執行一句var that = this
;也就是把this
給that
,這個時候this
是指的是object2
。再次調用object2.getNameFunc()()
時就是執行object2.getNameFunc()
返回來的函數」。that.name2=object2.name2
;實質上是閉包,使用了外部函數的that
變量。
代碼片斷三(對片斷二的改造):
var name3 = "The Window";
var object3 = {
name3: "My Object",
getNameFunc: function () {
return function () {
return this.name3;
}.bind(this);
}
};
console.log(object3.getNameFunc()); //ƒ (){return this.name3;}
console.log(object3.getNameFunc()()); //My Object
複製代碼
理解:與「代碼片斷二」同樣,只是片斷二是經過that=this
去改變this
的值,而片斷三是經過bind
綁定this
的值。看bind(this)
這裏的this
就是指這條語句object3.getNameFunc()
調用的對象object3
.因此經過這個手段去把this
指向了前面的對象object3
.再去調用返回的函數時,那麼this.name3=object3.name3
。
面試題2
<script>
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo); //bar
console.log(self.foo); //bar
(function() {
console.log(this.foo); //undefined 此時的this是window
console.log(self.foo); //bar 閉包能夠看到外部的局部變量
}()); //匿名函數自執行,是window上調用這個函數。
}
};
myObject.func();
//那麼如何修改呢?使得在自執行函數中的this.foo就是咱們想要的bar呢?
//提供兩種方法:
//case1:用call去指向this是誰
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo); // bar
console.log(self.foo); // bar
(function() {
console.log(this.foo); // bar
console.log(self.foo); // bar
}.call(this));
//myObject.func();這樣調用func(),那麼func()中的this就是前面的對象myObject。
}
};
myObject.func();
//case2:用bind去綁定this,但要注意bind是返回一個函數,故要bind(this)(),後一個括號表示函數調用。把bind(this)將拷貝一份並改變this的指向的函數執行。
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo); // bar
console.log(self.foo); // bar
(function() {
console.log(this.foo); // bar
console.log(self.foo); // bar
}.bind(this)());
}
};
myObject.func();
</script>
複製代碼
面試3 考察this
的指向: 難點:數組(類數組)中的元素當作函數調用時的this
指向 也就是,若是是調用數組(類數組)中的元素, 元素函數中的this
是這個數組(類數組)。
<script>
var length = 10;
function fn(){
console.log(this.length);
}
var obj = {
length: 5,
method: function (fn){ // [fn, 1, 2]
fn(); // 10
arguments[0](); // 3
}
};
obj.method(fn, 1, 2);
/*obj.method(fn, 1, 2);傳實參fn過去,此時fn拿到函數的地址值拷貝給形參fn,在執行fn()這裏調用是至關window調用fn,this指的是window。而不是obj不要感受是在obj裏面就是,迷惑你們的。this的指向永遠跟調用方式有關。
*另外,arguments[0]();調用時,這個時候是類數組中的元素調用,那麼這時的this是類數組自己,因此,數組.length是否是輸出類數組的長度。
*若是是調用數組(類數組)中的元素, 元素函數中的this是這個數組(類數組).爲何呢?看如下兩個例子:
* */
//例子1:
var obj = {
age : 100,
foo : function (){
console.log(this);
}
}
var ff = obj.foo;
ff(); //window
obj.foo(); //{age: 100, foo: ƒ}
obj["foo"](); //{age: 100, foo: ƒ}
//上面的這個例子沒有問題吧。很天然的。
//例子2:
var arr = [
function (){
console.log(this);
},function (){
}
];
var f = arr[0];
f(); //window
/*arr.0()--相似於這麼寫把,只是數組不容許這樣的語法--*/
arr[0](); //輸出數組自己:(2) [ƒ, ƒ] 。故驗證一句話:若是調用數組(類數組)中的元素時,那麼這時的this是數組(類數組)自己。
</script>
複製代碼
一個特別經典的總結:
從
a.b
就能夠看出做用域與做用域鏈,原型與原型鏈的知識。 (詳見Part1的1.1.3)
構造函數建立對象咱們先使用構造函數建立一個對象:
function Person() {
}
var person = new Person();
person.name = 'name';
console.log(person.name) // name
複製代碼
在這個例子中,Person
就是一個構造函數,咱們使用new建立了一個實例對象person
。
很簡單吧,接下來進入正題:【prototype】
任何的函數都有一個屬性prototype
,這個屬性的值是一個對象,這個對象就稱爲這個函數的原型對象。可是通常狀況,咱們只關注構造函數的原型。好比:
function Person() {
}
// 雖然寫在註釋裏,可是你要注意:prototype是函數纔會有的屬性
Person.prototype.name = 'name';
var person1 = new Person(); //person1是Person構造函數的實例
var person2 = new Person(); //person2是Person構造函數的實例
console.log(person1.name) // name
console.log(person2.name) // name
複製代碼
其實,函數的prototype
屬性指向了一個對象,這個對象正是調用該構造函數而建立的實例的原型,也就是這個例子中的person1
和person2
的原型。實例實際上是經過一個不可見的屬性[[proto]]
指向的。
你能夠這樣理解:每個JavaScript對象(null
除外)在建立的時候就會與之關聯另外一個對象,這個對象就是咱們所說的原型,每個對象都會從原型」繼承」屬性。
讓咱們用一張圖表示構造函數和實例原型之間的關係:
person1
person2
和
Person.prototype
之間的關係呢,這時候咱們就要講到第二個屬性:
[[proto]]
當使用構造函數建立對象的時候, 新建立的對象會有一個不可見的屬性[[proto]], 他會指向構造函數的那個原型對象。事實上,每個JavaScript對象(除了null)都具備的一個不可見屬性,叫[[proto]],這個屬性會指向該對象的原型。
爲了證實這一點,咱們能夠在火狐或者谷歌中輸入:
function Person() {
}
var person1 = new Person();
console.log(person1.__proto__ === Person.prototype); //true
複製代碼
因而咱們更新下關係圖:
constructor
屬性指向關聯的構造函數。
爲了驗證這一點,咱們能夠嘗試:
function Person() {
}
console.log(Person === Person.prototype.constructor); //true
複製代碼
因此再更新下關係圖:
function Person() {
}
var person1 = new Person();
//對象的__proto__屬性: 建立對象時自動添加的, 默認值爲構造函數的prototype屬性值(很重要)
console.log(person1.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 順便學習一個ES5的方法,能夠得到對象的原型
console.log(Object.getPrototypeOf(person1) === Person.prototype) //true
複製代碼
瞭解了構造函數、實例原型、和實例之間的關係,接下來咱們講講實例和原型的關係:【實例與原型】。當讀取實例的屬性時,若是找不到,就會查找與對象關聯的原型中的屬性,若是還查不到,就去找原型的原型,一直找到最頂層爲止。
舉個例子:
function Person() {
}
//往Person對象原型中添加一個屬性
Person.prototype.name = 'name';
//建立一個person1實例對象
var person1 = new Person();
//給建立的實例對象person1添加一個屬性
person1.name = 'name of this person1';
//查找person1.name,由於自己實例對象有,那麼就找到了自身實例對象上的屬性和屬性值
console.log(person1.name) // name of this person1
//刪除實例對象的屬性和屬性值
delete person1.name;
//查找屬性name,在實例對象自身上找不到,經過proto指向往原型鏈上找,在原型對象中找到
console.log(person1.name) // name
複製代碼
在這個例子中,咱們設置了person1
的name
屬性,因此咱們能夠讀取到爲name of this person1
,當咱們刪除了person1
的name
屬性時,讀取person1.name
,從person1
中找不到就會從person
的原型也就是person.__proto__ == Person.prototype
中查找,幸運的是咱們找到了爲name
,可是萬一尚未找到呢?原型的原型又是什麼呢?
var obj = new Object();
obj.name = 'name'
console.log(obj.name) // name
複製代碼
因此原型對象是經過Object
構造函數生成的,結合以前所講的一句很重要的話,幾乎就是涵蓋原型與原型鏈知識的始終的一句話,那就是:實例對象的proto
指向構造函數的prototype
。也就是說,Person.prototype
這個原型對象(實例原型)是經過Object
這個構造函數new出來的,也就是Person.prototype這個原型對象是Object
的實例,因此這個實例會有proto
屬性指向Object
構造函數的原型對象Object.prototype
。
這裏呢插入一句總結出來的話,逆推順推都是可行的,那就是:實例經過proto
這個屬性指向其構造函數的原型對象。因此咱們再更新下關係圖:
Object.prototype
的原型呢?
null
,嗯,就是
null
。因此查到
Object.prototype
就能夠中止查找了。因此最後一張關係圖就是:
那【原型鏈】是啥 ? 那就是由proto
這個屬性進行查找的一個方向這就是一條原型鏈。圖中由相互關聯的原型組成的鏈狀結構就是原型鏈,也就是藍色的這條線,都是經過proto
屬性進行查找的。
那麼訪問一個對象的屬性時,怎麼經過原型鏈去查找屬性或方法呢 ? 先在自身屬性中查找,找到返回。若是沒有, 再沿着proto
這條鏈向上查找, 找到返回。若是最終沒找到, 返回undefined
。
n
,
s
都是全局變量,而後經過對象字面量的方法去建立了一個對象。而後有一個構造函數(之因此是構造函數,是由於後面代碼有
new
的操做),這個構造函數就會有函數聲明提早,當構造函數聲明時,就會去在內存中建立一個
person
的函數對象,這個函數對象裏
只有prototype
屬性,去指向
person
的函數原型對象。要注意,如今尚未去執行裏面的代碼,只是函數聲明時建立了一個
person
的函數對象。後面就是new的實例對象,新
new
出來的實例對象
P2
P3
就會在內存中分配一塊內存去把地址值給它,如今纔會去執行構造函數中的代碼。因此只有
P2
P3
纔會 有
name
age
speak
屬性和方法。這些新
new
出來的實例對象就會有一個不可見的屬性
proto
,去指向這個原型對象。而最終這個
person
的函數原型對象會有指向一個
object
的原型對象,再上去其實就是
null
。這就一層一層往上走就是原型鏈,由於原型鏈,咱們纔會有繼承的特性。
注幾點:
P2
P3
實例對象雖然使用的是 Person
構造函數,可是對象建立出來以後,這個P2
P3
實例對象其實已經與 Person
構造函數(函數對象)沒有任何關係了,P2
P3
實例對象的[[ proto ]]
屬性指向的是 Person
構造函數的原型對象。new Person()
建立多個對象,則多個對象都會同時指向 Person
構造函數的原型對象。P2
P3
····這些實例對象就會共享這些在原型中添加的屬性和方法。也就是說,原型對象至關於公共的區域,全部的同一類的實例均可以去訪問到原型對象。P2
實例對象 中的一個屬性 gender
,若是在P2
對象中找到,則直接返回。若是 P2
對象中沒有找到,則直接去P2
對象的 [[proto]]
屬性指向的原型對象中查找,若是查找到則返回。(若是原型中也沒有找到,則繼續向上找原型的原型---原型鏈)。P2
對象只能讀取原型中的屬性 name
的值,並不能修改原型中的屬性name
的值。 P2.gender= "male"
; 並非修改了原型中的值,而是在 P2
對象中給添加了一個屬性 gender
。另外看看,原型與原型鏈的三點關注:
console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true
複製代碼
console.log(Function.__proto__===Function.prototype) //true
複製代碼
console.log(Object.prototype.__proto__) // null
複製代碼
instanceof
instanceof
是如何判斷的?
表達式: A instanceof B
若是B構造函數的原型對象(B.prototype
)在A實例對象的原型鏈(A.proto.proto·····
沿着原型鏈)上, 返回true
, 不然返回false
。(見下圖)
也就是說A實例對象的原型鏈上可能會有不少對象,只要B構造函數的原型對象有一個是在其原型鏈上的對象便可返回true
。
反過來講也同樣,實例對象A是否能夠經過proto
屬性(沿着原型鏈,A.proto.proto·····
)找到B.prototype
(B的原型對象),找到返回true
,沒找到返回false
.
注1:對實例對象的說明,事實上,實例對象有兩種。一種是咱們常常說的new
出來的實例對象(好比構造函數Person
, new
出來p1
p2
...,這些都是實例對象),另一種就是函數,函數自己也是實例,是Function
new
出來的。但咱們通常說的實例對象就是指new
出來的相似於p1
p2
這些的實例對象。
Function
是經過new
本身產生的實例(Function.proto===Function.prototype)
案例1:
//一個構造函數Foo
function Foo() { }
//一個f1實例對象
var f1 = new Foo()
//翻譯:f1是Foo的實例對象嗎?
//還記得我說過,一個實例對象經過proto指向其構造函數的原型對象上。
//深刻翻譯:f1這個實例對象經過proto指向是否能夠找到Foo.prototype上呢?
console.log(f1 instanceof Foo) // true
//這行代碼能夠得出,沿着proto只找了一層就找到了。
console.log(f1.__proto__ === Foo.prototype); // true
//翻譯:f1是Object的實例對象嗎?
//深刻翻譯:f1這個實例對象經過proto指向是否能夠找到Object.prototype上呢?
console.log(f1 instanceof Object) // true
//這兩行代碼能夠得出,沿着proto找了兩層才找到。事實上,f1.__proto__找到了Foo.prototype(Foo構造函數原型上),再次去.__proto__,找到了Object的原型對象上。見下圖。
console.log(f1.__proto__ === Object.prototype); // false
console.log(f1.__proto__.__proto__ === Object.prototype); // true
複製代碼
//這個案例的實質仍是那句話:一個實例對象經過proto屬性指向其構造函數的原型對象上。
//翻譯:實例對象Object是否能夠經過proto屬性(沿着原型鏈)找到Function.prototype(Function的原型對象)
console.log(Object instanceof Function) // true
//以上結果的輸出能夠看到下圖,Object.__proto__直接找到一層就是Function.prototype.(Object created by Function)可知Object構造函數是由Function建立出來的,也就是說,Object這個實例是new Function出來的。
console.log(Object instanceof Object) // true
//頗有意思。上面咱們已經知道Object這個實例是new Function出來的。也就是Object.proto指向Function.prototype。有意思的是,Function的原型對象又是Object原型對象的一個實例,也就是Function.prototype.proto 指向 Object.prototype .頗有意思吧,見下圖很更清楚這個「走向」。
console.log(Function instanceof Function) // true
//由這個可知,能夠驗證咱們的結論:Function是經過new本身產生的實例。 Function.proto===Function.prototype
console.log(Function instanceof Object) // true
//Function.proto.proto===Function.prototype (找了兩層)
//定義了一個Foo構造函數。由下圖可知,Foo.proto.proto.proto===null
function Foo() {}
console.log(Object instanceof Foo) // false
//這條語句要驗證的是,Object是否能夠經過其原型鏈找到Foo.prototype。
// Object.proto.proto.proto=null 並不會找到Foo.prototype。因此,返回FALSE。
複製代碼
(Object created by Function)
也就是說,對象是
new Function
獲得的。 繼續翻譯,對象是實例
Function
是構造函數。 繼續翻譯,對象這個實例有不可見屬性
proto
指向
Function
構造函數的原型對象
(Function.prototype)
。 故,函數與對象的關係是:函數更大,它包含對象。 這個我我的以爲很重要,務必理解透。
prototype
: 顯式原型屬性__proto__
: 隱式原型屬性{}
, 即用爲原型對象__proto__
: 在建立實例對象時被自動添加, 並賦值爲構造函數的prototype值__proto__
屬性, 它指向的就是原型對象__proto__
屬性就造成了一個鏈的結構---->原型鏈面試1:
/*阿里面試題*/
①function Person(){
②getAge = function (){
console.log(10)
}
③return this;
}
④Person.getAge = function (){
console.log(20);
}
⑤Person.prototype.getAge = function (){
console.log(30);
}
⑥var getAge = function (){
console.log(40)
}
⑦function getAge(){
console.log(50)
}
Q1:Person.getAge() // 20
Q2:getAge() // 40
Q3:Person().getAge() // 10
Q4:getAge() // 10
Q5:new Person().getAge() // 30
Q6:new Person.getAge(); // 20
複製代碼
總體代碼塊①定義了構造函數Person
②是在構造函數中有一個未聲明的變量,這個變量是引用變量,內容爲地址值。指向一個函數對象。又由於,未使用嚴格模式下,在函數中不使用var
聲明的變狼都會成爲全局變量。(注意這裏不是屬性,是全局變量)同時也要注意,這裏②和③的語句在解析到這裏後並無執行。執行的話就要看有沒有new
(做爲構造函數使用),或者有沒有加()調用(做爲普通函數使用)。 ③返回一個this
。這個this
是誰如今還不知道。須要明白js中的this
是動態的,因此根據上一節this
的總結才定位到this
究竟是誰。 ④Person.getAge
是典型的「對象.屬性(方法)」的形式,因此它是給Person
函數對象上添加一個getAge
的方法。等同於:
function Person.getAge(){
console.log(20);
}
複製代碼
函數名其實就是變量名。 ⑤在構造函數的原型中添加了getAge
的方法 ⑥這裏也是給一個全局變量賦值一個地址值,使其指向一個函數對象。注意,這裏var的變量會聲明提早。與代碼塊②區別,這裏當解析完後,getAge
已經指向一個函數對象啦。能夠看作:
function getAge(){
console.log(40)
}
複製代碼
⑦定義一個函數,函數也會聲明提早。在棧內存有getAge
,內容值爲一個地址值,指向一個函數對象。
Q1:對象.屬性方法()。代碼塊④產生的結果。 Q2:調用函數,全局做用域裏的。那只有代碼塊⑥產生結果。 Q3:Person().getAge()
。先看前面一部分Person()
,把Person
當作一個普通函數調用,執行Person
函數體對全局變量getAge
進行定義並從新指向,也就是Person()
執行了代碼塊②而覆蓋了代碼塊⑥的操做。又返回this,根據Person()
這種調用方式,可知this就是window
。因此就是「window.gerAge()」,因被覆蓋了,因此這行代碼執行結果是代碼塊②產生。 Q4:getAge()
至關於window.getAge()
; 仍是上一個語句的結果,代碼塊②產生結果。 Q5:new Person()
先看這部分,就是new出來一個實例,你能夠想成p1
,那麼p1.getAge();
p1
是一個Person
的實例,p1
中有不可見的[[proto]]
屬性,指向Person
的原型對象。那麼p1.getAge ()
,如今p1
自己找,找不到就沿着原型鏈(proto
指向鏈)去找,好找到了原型對向中有,由於代碼塊⑤產生做用。 Q6:new Person.getAge();
能夠把Person.getAge
當作一個對象,去new
它,是否是相似於咱們日常var p1=new Person()
;這樣的操做,因此咱們把Person.getAge
看作一個構造函數去new
它。由上面對代碼塊④的理解,能夠看作那樣的函數,因此結果就是代碼塊④產生的結果。
面試題2:
function A () {
}
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
};
var c = new A();
console.log(b.n, b.m, c.n, c.m); //1 undefined 2 3
//見下圖:
複製代碼
//與上題的區別在於如何理解a.x的執行順序
<script>
var a = {n: 1};
var b = a;
a.x = a = {n: 2}; //先定義a.x再去從右往左賦值操做。
console.log(a.x); // undefined 對象.屬性 找不到 是返回undefined 變量找不到則報錯!
console.log(b); // {n :1, x : {n : 2}}
</script>
//見下圖分析
複製代碼
面試題4:
//構造函數F
function F (){};
Object.prototype.a = function(){
console.log('a()')
};
Function.prototype.b = function(){
console.log('b()')
};
//new一個實例對象f
var f = new F();
f.a(); //a()
f.b(); //報錯,找不到
F.a(); //a()
F.b(); //b()
複製代碼
變量聲明提高
var
定義(聲明)的變量, 在定義語句以前就能夠訪問到undefined
console.log(c); //報錯,c is not defined.
console.log(b); //undefined
var b=0;
c=4;
console.log(c); //4 意外的全局變量->在ES5的嚴格模式下就會報錯。
console.log(b); //0
複製代碼
函數聲明提高
先有變量提高, 再有函數提高
案例一:
var a = 3;
function fn () {
console.log(a); //undefined
var a = 4
}
fn();
//上面這段代碼至關於
var a = 3;
function fn () {
var a;
console.log(a); //undefined
a = 4
}
fn();
複製代碼
案例二:
console.log(b) //undefined 變量提高
fn2() //可調用 函數提高
fn3() //不能調用,會報錯。 fn3是一個函數表達式,並不會函數提高,實際上他是變量提高。
var b = 3
function fn2() {
console.log('fn2()')
}
var fn3 = function () {
console.log('fn3()')
}
複製代碼
問題: 變量提高和函數提高是如何產生的? An:由於存在全局執行上下文和函數執行上下文的預處理過程。因此咱們就來學習下一節的執行上下文。
代碼分類(位置)
執行上下文分爲全局執行上下文和函數執行上下文
全局執行上下文
window
肯定爲全局執行上下文對象(虛擬的)var
定義的全局變量==>值爲undefined
, 並添加爲window
的屬性function
聲明的全局函數==>賦值(fun
), 添加爲window
的方法this
==>賦值(window
)//全局執行上下文
console.log(a1); //undefined
console.log(a2); //undefined
a2(); //也會報錯,a2不是一個函數
console.log(a3); //ƒ a3() {console.log('a3()')}
console.log(a4) //報錯,a4沒有定義
console.log(this); //window
var a1 = 3;
//函數表達式,其實是變量提高。而不是函數提高。
var a2 = function () {
console.log('a2()')
};
function a3() {
console.log('a3()')
}
a4 = 4;
複製代碼
函數執行上下文
arguments
==>賦值(實參列表), 添加爲執行上下文的屬性var
定義的局部變量==>undefined
, 添加爲執行上下文的屬性function
聲明的函數 ==>賦值(fun
), 添加爲執行上下文的方法this
==>賦值(調用函數的對象)//函數執行上下文
function fn(a1) {
console.log(a1); //2 實參對形參賦值
console.log(a2); //undefined 函數內部局部變量聲明提高
a3(); //a3() 可調用 函數提高
console.log(arguments); //類數組[2,3]
console.log(this); //window
var a2=3;
function a3() {
console.log("a3()");
}
}
fn(2,3); //執行,不執行不會產生函數執行上下文
複製代碼
全局執行上下文和函數執行上下文的生命週期
全局 : 準備執行全局代碼前產生, 當頁面刷新/關閉頁面時死亡
函數 : 調用函數時產生, 函數執行完時死亡
window
)肯定後, 將其添加到棧中(壓棧)window
<script type="text/javascript">
//1. 進入全局執行上下文
var a = 10;
var bar = function (x) {
var b = 5;
foo(x + b) //3. 進入foo執行上下文
};
var foo = function (y) {
var c = 5;
console.log(a + c + y)
};
bar(10); //2. 進入bar函數執行上下文(注:函數執行上下文對象在函數調用時產生,而不是函數聲明時產生)
</script>
複製代碼
以上這種狀況整個過程產生了3個執行上下文 調用一次函數產生一個執行上下文 若是在上面代碼最後一行的bar(10)
,再調用一次bar(10)
,那麼就會產生5個上下文。 由於第一個bar(10)
產生一個函數上下文 在bar
函數中調用foo
,又產生一個函數執行上下文。 那麼如今又調用bar(10)
,與上面一個樣會產生兩個上下文,加起來4個函數執行上下文。 最後加上window
的全局變量上下文,一共五個執行上下文。
window
的全局上下文,而後執行
bar()
會把
bar
函數執行上下文壓入棧中。
bar
中調用
foo
,把
foo
函數執行上下文壓入棧中,
foo
函數執行完畢,釋放,便會把
foo
函數執行上下文
pop
(推出來)。逐漸
bar
執行完畢,
pop
出
bar
函數執行上下文,最後只剩下
window
上下文。
註解2:假設一個狀況:f1()
函數中會調用f2()
和f3()
函數。那麼在當前時刻棧中可最多達到幾個上下文? An: 當f1()
執行,會先調用f2()
,調用完後,f2()
已經完成了使命,它的生命週期就結束了,因此棧 就會釋放掉他,在執行f3()
,因此棧中也就最多三個上下文。f3()
f1()
window
.
註解3:假設另外一個狀況:f1()
函數中會調用f2()
, f2()
中會調用f3()
函數。那麼在當前時刻棧中可最多達到幾個上下文 ? An: 當f1()
執行,會先調用f2()
,執行f2()
時要調用f3()
,因此,棧中可達到4個上下文。f3()
f2()
f1()
window .
面試題1:執行上下文棧
<script type="text/javascript">
console.log('global begin: '+ i); //undefined 變量提高
var i = 1;
foo(1);
function foo(i) {
if (i == 4) {
return;
}
console.log('foo() begin:' + i);
foo(i + 1);
console.log('foo() end:' + i);
}
console.log('global end: ' + i) //1 全局變量i,其餘的函數中的i當執行結束後就銷燬了。
複製代碼
執行結果:
 global begin: undefined
foo() begin:1
foo() begin:2
 foo() begin:3
 foo() end:3
 foo() end:2
 foo() end:1
 global end: 1
複製代碼
一共產生5個上下文: 分析見下圖,我畫的很清楚了。這張圖畫了12min。主要就是入棧出棧,在出棧前,回溯原來的那個函數,那個函數執行上下文還在,若是還有 沒有執行完的語句 會在這個時候執行。當剩餘的語句已經執行完了,那麼這個函數的執行上下文生命週期結束,釋放出棧。想一想咱們遞歸調用去求階乘的例子,思想是同樣的。
面試題2:變量提高和函數提高(執行上下文)
function fn(a){
console.log(a); // 輸出function a的源碼,a此時是函數a
var a = 2;
function a(){
}
console.log(a); // 2
}
fn(1);
複製代碼
考察點: 聲明提早 難點: 函數優先
調用一開始, 就會先建立一個局部變量a, (由於a是形參), 而後把實參的值1賦值給a
,a= 1
幾乎在同一時刻,那麼一瞬間,開始處理函數內變量提高和函數提高 此時,a
由於函數提高已經變成了a = function(){}
以上這些過程都是函數執行上下文的預處理過程 接下來,纔是正式執行內部函數的代碼。 console.log(a);
此時輸出的就是function源碼ƒ a(){}
結尾的輸出語句便輸出a = 2
。
測試題1: [考查知識點]先預處理變量, 後預處理函數
function a() {} //函數提高
var a; //變量提高
//先預處理變量, 後預處理函數。也就是,函數提高會覆蓋變量提高。
console.log(typeof a); //function
複製代碼
測試題2:[考查知識點] 變量預處理, in操做符 (在window上能不能找到b,無論有沒有值)
if (!(b in window)) {
var b = 1;
//在ES6以前沒有塊級做用域,因此這個變量b 至關於window的全局變量
}
console.log(b); //undefined
複製代碼
測試題3: [考查知識點]預處理, 順序執行 這個題筆者認爲出的至關好。混亂讀者的視角。固然再次強調,面試題是專門命題出來考查的,實際開發上可能有些不會這麼用。但主要做用就是深刻理解。
var c = 1;
function c(c) {
console.log(c);
var c = 3;
}
c(2); //報錯。 c is not a function
//這個題包含了變量和函數聲明提高的問題,就是等價於如下的代碼:
var c; //變量提高
function c(c) { //函數提高,覆蓋變量提高
console.log(c);
var c = 3; //函數內部的局部變量(在棧內存的封閉內存空間裏,外面看不到)
}
c=1;//開始真正執行代碼var c = 1
console.log(c);
c(2); //c is not a function c是一個變量,值爲number類型的數值.怎麼能夠執行?
複製代碼
1.理解:
2.分類:
ES6
有了!)3.做用
b
,那麼在函數體中能不能有變量b
,固然能夠,這就是分隔變量。給個案例:
var a = 10,
b = 20
function fn(x) {
var a = 100,
c = 300;
console.log('fn()', a, b, c, x)
function bar(x) {
var a = 1000,
d = 400
console.log('bar()', a, b, c, d, x)
}
bar(100)
bar(200)
}
fn(10);
複製代碼
輸出結果:
fn() 100 20 300 10
bar() 1000 20 300 400 100
bar() 1000 20 300 400 200
複製代碼
4.做用域的圖解以下:
1.區別1
2.區別2
3.聯繫
4.做用域與執行上下文圖解以下:
1.理解
2.查找一個變量的查找規則
3.做用域鏈的圖解以下:
面試題1:做用域
<script type="text/javascript">
var x = 10;
function fn() {
console.log(x); //10
}
function show(f) {
var x = 20;
f();
}
show(fn);
</script>
複製代碼
記住: 做用域是代碼一編寫就肯定下來了,不會改變。產生多少個做用域?n+1
. n
就是多少個函數,1
就是指的是window
。查找變量就是沿着做用域查找,而做用域是一開始就肯定了,與哪裏調用一點關係都沒有。 見圖解:
<script type="text/javascript">
var fn = function () {
console.log(fn) //output: ƒ () {console.log(fn)}
}
fn()
var obj = {
fn2: function () {
console.log(fn2) //報錯,fn2 is not defined
console.log(this.fn2)//輸出fn2這個函數對象
}
}
obj.fn2()
</script>
複製代碼
報錯緣由:由於首先在這個匿名函數做用域找,找不到去上一層全局找,沒找到,報錯。找fn2
是沿着做用域查找的! 輸出fn2
這個函數對象的緣由:若是要找到obj
屬性fn2
,則用this.fn2()
,讓其用this
這個指針指向obj
,在obj
這個對象中找fn2
屬性。
面試題3:考察連續賦值隱含的含義
(function(){
var a = b = 3;
})();
console.log(a); // 報錯 a is not defined
console.log(b); // 3
複製代碼
理解:首先,賦值從右向左看。b = 3
由於沒var 因此至關於在全局做用域中添加b
,賦值爲3
。如今。看前面var a=
的部分,a有var,那麼a就是局部變量放在棧內存的封閉內存空間上。var a=b
,b
是變量,是基本數據類型的變量。它的內存中的內容值就是基本數據類型值,故拷貝一份給a
。局部變量中a=3
匿名函數自執行,會有一個函數執行上下文對象。當函數執行完成,就會把執行上下文棧彈出這個上下文對象。就再也訪問不到。
因此,在函數自執行結束後,再執行輸出a
的語句。a
壓根就找不到,根本沒定義。b
由於是全局變量仍是能夠找到滴。
注意擴展,這裏只有在非嚴格模式下,纔會把b
當作全局變量。若在ES5中嚴格模式下,則會報錯。
面試4:考慮返回值問題
function foo1(){
return {
bar: "hello"
}
}
function foo2(){
return
{
bar: "hello"
}
}
console.log(foo1()); // 返回一個對象 {bar:'hello'}
console.log(foo2()); // undefined
複製代碼
解釋:由於foo2
函數return
後面少了分號,在js引擎解析時,編譯原理的知識可知,在詞法分析會return
後面默認加上分號,因此,後面那個對象壓根不執行,壓根不搭理。因此啊,當return
時返回的就是undefined
。
面試5:函數表達式的做用域範圍
<script>
console.log(!eval(function f() {})); //false
var y = 1;
if (function f(){}){
y += typeof f;
}
console.log(y); // 1undefined
</script>
複製代碼
Code 1:
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍歷加監聽
for (var i = 0,length=btns.length; i < length; i++) {
var btn = btns[i]
btn.onclick = function () {
alert('第'+(i+1)+'個')
}
}
</script>
複製代碼
輸出結果:無論點擊哪一個button
,都是輸出「第4個」。由於for
循環一下就執行完了,但是btn.onclick
是要等到用戶事件觸發的,故這個時候i
是3
.永遠輸出「第4個」。 一些細節問題:在這個過程當中,產生了多少個i
?一個i
,i
是全局變量啊。 事件模型的處理: 當事件被觸發時,該事件就會對此交互進行響應,從而將一個新的做業(回調函數)添加到做業隊列中的尾部,這就是js關於異步編程最基本的形式。 事件能夠很好的工做於簡單的交互,但將多個分離的異步調用串聯在一塊兒就會很麻煩,由於你必需要追蹤到每一個事件的事件對象(例如上面的btn
).此外你還要確保全部的事件處理程序都能在事件第一次觸發以前被綁定完畢。例如,若btn
在onclick
被綁定前點擊,那麼就不會有任何的事情發生。所以,雖然在響應用戶交互或相似的低頻功能時,事件頗有用,但它面對更復雜的需求時仍然不夠靈活。 因此,從這個例子不只僅是對遍歷加監聽/閉包等理解。從這裏也能夠說明事件對象和事件機制的問題。因此,在ES6中會有promise
和異步函數進行更多更復雜需求上的操做。
Code 2 經過對象.屬性
保存i
:
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍歷加監聽
for (var i = 0,length=btns.length; i < length; i++) {
var btn = btns[i]
//將btn所對應的下標保存在btn上(解決方式1)
btn.index = i
btn.onclick = function () {
alert('第'+(this.index+1)+'個')
}
}
</script>
複製代碼
這個時候就是咱們想要的結果,點哪一個i
,button
就輸出第幾個。
Code 3 經過ES6的塊級做用域 let
:
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍歷加監聽
for (let i = 0,length=btns.length; i < length; i++) { //(解決方式二)
var btn = btns[i]
btn.onclick = function () {
alert('第'+(i+1)+'個')
}
}
</script>
複製代碼
在ES6中引入塊級做用域,使用let
便可。
Code 4 利用閉包解決
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//利用閉包
for (var i = 0,length=btns.length; i < length; i++) { //這裏的i是全局變量
(function (j) { //這裏的j是局部變量
var btn = btns[j]
btn.onclick = function () {
alert('第'+(j+1)+'個') //這裏的j是局部變量
}
})(i); //這裏的i是全局變量
}
</script>
複製代碼
for
循環裏有兩個函數,btn.click
這個匿名函數就是一個閉包。它訪問了外部函數的變量。 產生幾個閉包?3個閉包(外部函數執行幾回就產生幾個閉包)。每一個閉包都有變量j
,分別保存着j=0
,j=1
,j=2
的值。故能夠實現這樣的效果。以前之因此出問題,是由於都是用着全局變量的i,同一個i值。 閉包有沒有被釋放?沒有,一直存在。咱們知道閉包釋放,那就是讓指向內部函數的引用變量爲null
便可。可是此時btn.onclick
一直引用這內部函數(匿名函數),故其閉包不會被釋放。 閉包應不該該被釋放?不該該。由於一個頁面的一個button
是要一直存在的,頁面顯示過程當中,button
要一直關聯着這個閉包。才能讓每點擊button1
就alert
(第1個)這樣的結果。不可能讓button
點擊了一次就失效吧。那麼假設要釋放這些閉包,那就讓btn.onclick=null
便可。 閉包的做用?延長局部變量j
的生命週期。
1.如何產生閉包?
b
,而內部函數中沒有引用b
,則不會產生閉包。2.閉包究竟是什麼?
閉包是指有權訪問另外一個函數做用域中的變量的函數。
能夠理解爲:
包含了那個局部變量的容器(不必定是對象,相似對象)
他被內部函數對象引用着
怎麼判斷閉包存在否?最終就是判斷函數對象有沒有被垃圾回收機制。
3.產生閉包的條件?
案例1:
function fn1 () {
var a = 2
var b = 'abc'
function fn2 () { //執行函數定義就會產生閉包(不用調用內部函數)
console.log(a) //引用了外部函數變量,若裏面沒有引用任何的外部函數變量(函數)則不會產生閉包
}
// fn2() 內部函數能夠不執行,也會產生閉包。只要執行了內部函數的定義就行。但如果函數表達式呢?
}
fn1(); //外部函數要執行哦,不然不會產生閉包
複製代碼
案例2:
function fun1() {
var a = 3
var fun2 = function () {
console.log(a)
}
}
fun1()
//這樣子經過函數表達式定義函數,若沒有在裏面調用內部函數,則不會產生閉包。
複製代碼
案例3:
function fun1() {
var a = 3
var fun2 = function () {
console.log(a)
}
fun2()
}
fun1()
//這樣子經過函數表達式定義函數,但在裏面調用了內部函數,那麼這個狀況是能夠產生閉包的。
複製代碼
函數表達式不一樣於函數聲明。函數聲明要求有名字,但函數表達式不須要。沒有名字的函數表達式也叫作匿名函數(anonymous function),匿名函數有時候也叫拉姆達函數,匿名函數的 ·name· 屬性是空字符串。
以上這些案例,只是輔助理解。並無實際上的應用,下面就來講說閉包實際能夠應用的地方。
案例1:將函數做爲另外一個函數的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
複製代碼
深刻理解: 問題一:有沒有產生閉包? An:條件一,函數的嵌套。外部函數fn1
,內部函數fn2
,條件一知足;條件二,內部函數引用了外部函數的數據(變量/函數)。a
就是外部函數的數據變量。條件二知足。條件三,執行外部函數。var f = fn1()
其中的fn1()
是否是執行了,外部函數執行了。賦值給f
變量是由於外部函數fn1
在執行後返回一個函數,用全局變量f
來保存其地址值。條件三知足。綜上所述,產生了閉包。
問題二:產生了幾個閉包? 產生了一個閉包。咱們根據上一節的知識可知:執行函數定義就會產生閉包。那麼執行函數定義是否是隻要執行外部函數便可,由於外部函數一執行,就會有函數上下文對象,就會函數聲明提早,也就是執行了函數定義。那麼,這個時候執行了幾回外部函數?是否是一次。執行了一次外部函數,也就是聲明函數提早了一次,執行函數定義這個操做作了一次,故只產生了一個閉包。也可得出結論,外部函數執行幾回,就產生幾個閉包。跟內部函數執行幾回沒有關係(前提,在能夠生成閉包的狀況下)
問題三:調用內部函數爲何能夠讀出a的最新值? 從結果能夠知道,f()
,f()
是否是在調用了兩次內部函數,從輸出的結果看,a
每次輸出最新值。這就能夠知道,在執行內部函數的時候,a
並無消失。記住這點,這就是閉包的本質做用。
問題四:那麼若是我如今在以上代碼最後(分別輸出3
,4
語句後面)繼續加入
var h =fn1();
h();
f();
複製代碼
這個時候會輸出什麼? An:h()
會輸出3
. f()
會輸出5
. 由於:var h = fn1()
又執行了一次,h接收返回值函數對象(內部函數),也就是在這個時候產生了新的一個閉包。當調用內部函數時,h()
,就會有新的函數上下文對象產生,a
值就會從初始值開始記錄。當調用f()
時,這個時候仍是在上一個閉包的狀態下,那個做用域並無消失,故還在原先的基礎上改變a
值。
案例2. 將函數的實參傳遞給另外一個函數調用(★★★)
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)
}, time)
}
showDelay('my name is ly', 2000)
複製代碼
這個例子說明了,咱們要使用閉包不必定要return
出去。只要這個函數對象被引用着就行。return
的話那我再外面用變量接收一下就引用着了。可是我使用定時器,定時器模塊式在瀏覽器分線程運行着的,定時器這個回調函數就是定時器模塊保存管理着。
深刻理解: 問題一:有沒有產生閉包? An:條件一,函數的嵌套。外部函數showDelay
,內部函數定時器的回調函數,條件一知足;條件二,內部函數引用了外部函數的數據(變量/函數)。msg
就是外部函數的數據變量,而在回調函數中用了。注意,time
不是哦,time
仍是在外部函數用的,在內部函數中並無用到外部函數的time變量。是由於msg
變量才知足條件二。條件三,執行外部函數。showDelay('my name is ly', 2000)
執行了,外部函數執行了。可是注意回調函數沒有聲明提高,故還要等2000ms
後觸發進行調用回調函數。這個時候內部函數才執行。條件三知足(這個相似於函數表達式狀況,若是是函數表達式,那麼不只僅要外部函數要執行,內部函數表達式定義的函數也要有執行,只有這樣纔會出現閉包。若是是函數聲明定義的函數,那麼就會有函數執行上下文去建立,函數提高,故在執行函數定義的時候就會出現閉包)。綜上所述,產生了閉包。
1.使用函數內部的局部變量在函數執行完後, 仍然存活在內存中(延長了局部變量的生命週期) 原本,局部變量的生命週期是否是函數開始執行到執行完畢,局部變量就自動銷燬啦。可是,經過閉包能夠延長局部變量的生命週期,函數內部的局部變量能夠在函數執行完成後繼續存活在內存中。那就是經過閉包,具體怎樣的內部機制見下。
2.讓函數外部能夠操做(讀寫)到函數內部的數據(變量/函數) 原本,函數內部是能夠經過做用域鏈去由內向外去找數據(變量/函數),是能夠訪問到函數外部的。可是反過來是不行的,函數外部能訪問到內部的數據(變量/函數)嗎?
function fun1() {
var a='hello world'
}
console.log(a); //報錯 a is not defined
//函數外部不能訪問函數內部的數據
複製代碼
因此,函數外部不能訪問函數內部的數據。可是,能夠經過閉包去訪問到函數內部的數據。具體的內部機制又是怎樣的呢?見下。
//詳解閉包的做用(重要)
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
function fn3() {
a--
console.log(a)
}
return fn3
}
var f = fn1()
f() // 1
f() // 0
複製代碼
問題1. 函數fn1()
執行完後, 函數內部聲明的局部變量是否還存在? An: 通常是不存在, 存在於閉包中的變量纔可能存在。像fn2
fn3
變量就自動銷燬了。由於函數內的局部變量的生命週期就是函數開始執行到執行完畢的過程。那像fn2
這個函數對象也會被垃圾回收機制回收,由於沒有變量去引用(指向)fn2
函數對象。可是fn3
這個對象還在,根本緣由是由於語句 var f = fn1();
fn( )
執行完畢返回一個fn3
的地址值而且賦值給全局變量f,那麼全局變量f就會指向他,因此,這個fn3
這個函數對象不會被垃圾回收機制回收。 但我在回答這個問題時,是說存在於閉包中的變量纔可能存在。爲何可能呢?由於若是我把語句var f = fn1();
改爲fn1()
,這個時候仍是沒有變量去引用,因此這時仍是會被回收的。見下圖。
fn3
函數對象一直會有引用,閉包就會存在。這時我只要將
f=null
,這個時候
fn3
函數對象就沒有被
f
引用,因此會被垃圾回收機制回收,故此時這個閉包死亡。
問題2:在函數外部能直接訪問函數內部的局部變量嗎? An: 不能, 但咱們能夠經過閉包讓外部操做它.
function fn1() {
//此時閉包就已經產生了(函數提高, 內部函數對象已經建立了)
var a = 2
function fn2 () {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //閉包死亡(包含閉包的函數對象成爲垃圾對象)
複製代碼
閉包應用:
jQuery
)大量使用了閉包閉包的應用之一:定義JS模塊
要具備特定功能的js文件
將全部的數據和功能都封裝在一個函數內部(私有的)(函數內部會有做用域與做用域鏈的概念,函數內部的數據就是私有的,外部訪問不到。)
只向外暴露一個包含n個方法的對象(暴露多個行爲)或函數(暴露一個行爲)
模塊的使用者, 只須要經過模塊暴露的對象調用方法來實現對應的功能
自定義JS模塊一:
function myModule() {
//私有數據
var msg = 'Hello world';
//操做數據的函數
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露對象(給外部使用的方法)
return {
doSomething: doSomething,
doOtherthing: doOtherthing
}
}
//怎麼使用?在html頁面中
var module = myModule()
module.doSomething()
module.doOtherthing()
複製代碼
自定義JS模塊二:
(function () {
//私有數據
var msg = 'My atguigu'
//操做數據的函數
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露對象(給外部使用的方法)
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})();
//怎麼使用?在html頁面中
myModule2.doSomething()
myModule2.doOtherthing()
//這種自定義模塊相對而言更好,由於不須要先調用外部函數,直接使用 myModule2.doSomething()更加方便。
複製代碼
1.缺點
2.解決
<script type="text/javascript">
function fn1() {
var arr = new Array[100000]
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
//這裏是有閉包的,arr一直沒有釋放,很佔內存。
//如何解決?很簡單。
f = null //讓內部函數成爲垃圾對象-->回收閉包
</script>
複製代碼
理解:
做用:
寫一個閉包程序
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f();
f();
複製代碼
閉包應用:
jQuery
)大量使用了閉包缺點:
f = null;
//讓內部函數對象成爲垃圾對象面試1:考察閉包
function foo(){
var m = 1;
return function (){
m++;
return m;
}
}
var f = foo(); //這會造成一個閉包 (調用一次外部函數)
var f1 = foo(); //這會造成一個閉包 (調用一次外部函數)
/*不一樣閉包有不一樣做用域。同一個閉包能夠訪問其最新的值。--這句話知識一個表面現象,結合上面的案例去發現深刻的步驟,這個過程是如何執行的?*/
console.log(f()); // 2
console.log(f1()); // 2
console.log(f()); // 3
console.log(f()); // 4
複製代碼
面試2:閉包相關知識
<script>
function fun(n, o){
console.log(o); //實則就是輸出閉包中的變量值,n是閉包引用的變量。延長的是n的變量生命週期。
return {
fun: function (m){
return fun(m, n);
}
}
}
/*注意以上代碼段是有閉包的,return fun(m,n)中的n是用到了外部函數的變量n*/
//測試一:
var a = fun(0); // undefined
a.fun(1); // 0 執行這裏實則是產生了新的閉包,但沒有變量去指向這個內部函數產生的閉包故立刻就消失啦。
a.fun(2); // 0
a.fun(3); // 0
/*最後三行語句一直用的閉包是fun(0)產生的閉包*/
//測試二:
var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2
/*產生了四個閉包,也就是外部函數fun(n,o)調用過4次。*/
// 測試三:
var c = fun(0).fun(1); // undefined 0
c.fun(2) // 1
c.fun(3) // 1
/*最後兩行語句一直用的閉包是fun(0).fun(1)產生的閉包,故其語句*/
</script>
複製代碼
面試3:寫一個函數, 使下面的兩種調用方式都正確
console.log(sum(2,3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
複製代碼
答案:
<script>
function sum(){
if(arguments.length == 2){
return arguments[0] + arguments[1];
}else if(arguments.length == 1){
var first = arguments[0];
return function (a){
return first + a;
}
}
}
</script>
複製代碼
此文檔爲呂涯原創,可任意轉載,但請保留原連接,標明出處。 文章只在CSDN和掘金第一時間發佈: CSDN主頁:https://blog.csdn.net/LY_code 掘金主頁:https://juejin.im/user/5b220d93e51d4558e03cb948 如有錯誤,及時提出,一塊兒學習,共同進步。謝謝。 😝😝😝