一、原型鏈(家族族譜)
概念:JS里面的對象可能會有父對象,父對象還會有父對象,。。。。。祖先
- 根本:繼承
屬性:對象中幾乎都會有一個__proto__屬性,指向他的父對象
意義:可以實現讓該對象訪問到父對象中相關屬性
- 根對象:Object.prototype
var arr=[1,3,5]
arr.__proto__:Array.prototype
arr.__proto__.__proto__就是找到了根對象
function Animal(){} var cat=new Animal(); //cat.__proto__:Animal.prototype //cat.__proto__.__proto__:根對象
錯誤的理解:在js中,萬物繼承自Object? -->在js中萬物繼承自Object.prototype
二、作用域
(一)、變量作用域
變量作用域的概念:就是一個變量可以使用的范圍
JS中首先有一個最外層的作用域:稱之為全局作用域
JS中還可以通過函數創建出一個獨立的作用域,其中函數可以嵌套,所以作用域也可以嵌套
var age=18; //age是在全局作用域中聲明的變量:全局變量 function f1(){ console.log(name); //可以訪問到name變量 var name="周董" //name是f1函數內部聲明的變量,所以name變量的作用域就是在f1函數內部 console.log(name); //可以訪問到name變量 console.log(age); //age是全局作用域中聲明的,所以age也可以訪問 } console.log(age); //也可以訪問
//-->1級作用域 var gender="男"; function fn(){ console.log(age); //因為age是在fn作用域內聲明的 //age:undefined:既然有值就是可以訪問 console.log(height);//height不是在該作用域內部聲明的,所以不能訪問 //-->2級作用域 return function(){ //-->3級作用域 var height=180; } var age=5; } //注意:變量的聲明和賦值是在兩個不同時期的 function fn(){ console.log(age); //undeinfed var age=18; console.log(age); //18 } //fn函數執行的時候,首先找到函數內部所有的變量、函數聲明,把他們放在作用域中,給變量一個初始值:undefined -->變量可以訪問 //逐條執行代碼,在執行代碼的過程中,如果有賦值語句,對變量進行賦值 function fn(){ var age; //初始值:undefined console.log(age); //undeinfed age=18; //修改了變量的值 console.log(age); //18 }
(二)、作用域鏈
由於作用域是相對於變量而言的,而如果存在多級作用域,這個變量又來自於哪里?這個問題就需要好好地探究一下了,我們把這個變量的查找過程稱之為變量的作用域鏈
作用域鏈的意義:查找變量(確定變量來自於哪里,變量是否可以訪問)
簡單來說,作用域鏈可以用以下幾句話來概括:(或者說:確定一個變量來自於哪個作用域)
查看當前作用域,如果當前作用域聲明了這個變量,就確定結果
查找當前作用域的上級作用域,也就是當前函數的上級函數,看看上級函數中有沒有聲明
再查找上級函數的上級函數,直到全局作用域為止
如果全局作用域中也沒有,我們就認為這個變量未聲明(xxx is not defined)
function fn(callback){ var age=18; callback() } fn(function(){ console.log(age); //分析:age變量: //1、查找當前作用域:並沒有 //2、查找上一級作用域:全局作用域 //-->難點:看上一級作用域,不是看函數在哪里調用,而是看函數在哪里編寫 //-->因為這種特別,我們通常會把作用域說成是:詞法作用域 })舉例1:
var name="張三"; function f1(){ var name="abc"; console.log(name); //abc } f1();舉例2:
var name="張三"; function f1(){ console.log(name); //underfind var name="abc"; } f1();舉例3:
var name="張三"; function f1(){ return function(){ console.log(name); //underfind } var name="abc"; } var fn=f1(); fn();舉例4:
var name="張三"; function f1(){ return { say:function(){ console.log(name); //underfind var name="abc"; } } } var fn=f1(); fn.say()
三、閉包
(一)、認識閉包
var divs=document.getElementsByTagName("div"); for (var i = 0; i < divs.length; i++) { const element = divs[i]; element.onclick=function(){ //i是來自於全局作用域 alert(i) } } // 執行完for循環之后,i的值已經變成了5
閉包的解決
var divs=document.getElementsByTagName("div"); for (var i = 0; i < divs.length; i++) { const element = divs[i]; //閉包的解決方案 element.onclick=(function(j){ return function(){ //i是來自於全局作用域 alert(j) } })(i); }
例一:
function fn(){ var a=5; return function(){ a++; console.log(a); //a變量肯定是可以訪問的 } } var f1=fn(); //f1指向匿名函數 f1(); //6 f1(); //7 f1(); //8 //代碼執行到8行fn函數執行完畢,返回匿名函數 // -->一般認為函數執行完畢,變量就會釋放,但是此時由於js引擎發現匿名函數要使用a變量,所以a變量並不能得到釋放,而是把a變量放在匿名函數可以訪問到的地方去了 // -->a變量存在於f1函數可以訪問到的地方,當然此時a變量只能被f1函數訪問
例二:
function fn(){ var a=5; return function(){ a++; console.log(a); //a變量肯定是可以訪問的 } } var f1=fn(); //f1指向匿名函數 f1(); //6 f1(); //7 f1(); //8 //把a變量的值放在f1函數可以訪問到的地方 var f2=fn(); f2(); //6 f2(); //7 f2(); //8 //又一次執行了fn,又初始化了一個新的a變量,值為5;返回匿名函數f2,並且把新的a變量放在了f2可以訪問到的地方 var f3=fn(); f3(); //6 //又一次執行了fn,又初始化了一個新的a變量,值為5;返回匿名函數f2,並且把新的a變量放在了f2可以訪問到的地方
例三:
function q1(){ var a={}; return a; } var r1=q1(); var r2=q1(); console.log(r1==r2); //fasle function q2(){ var a={} return function(){ return a; } } var t3=q2();//創建一個新的a對象,把a對象放在t3可以訪問到的位置 var o5=t3(); //返回值a就是那個a var w3=q2();//創建了一個新的a對象,把新的a對象放在w3可以訪問到的位置 var o8=w3();//此時獲取到的是一個新的a對象 console.log(o5==o8); //false
(二)、閉包的應用場景
-
模塊化
-
防止變量被破壞
//模塊化思想:也是一種設計模式 var ktv=(function KTV(){ //為了保護leastPrice變量,將它放在函數內部 var leastPrice=1000; var total=0; return { //購物 buy:function(price){ total+=price; }, //結賬 pay:function(){ if(total<leastPrice){ console.log('請繼續購物'); }else{ console.log('歡迎下次光臨'); } }, editLeast:function(id,price){ if(id===888){ leastPrice=price; console.log("現在最低消費金額為:",leastPrice); }else{ console.log('權限不足'); } } } })() //假設:來了朋友要來唱K //——>可能老板需要去修改最低消費的金額 //-->但是並不能讓老板直接去修改leastPrice,或者說不能把leastPrice作為全局變量
(三)、閉包問題的產生原因
- 函數執行完畢后,作用域中保留了最新的a變量的值
function f1(){ var a=5; return function(){ a++; console.log(a); } } var q1=f1(); //要想釋放q1里面保存的a,只能通過釋放q1 q1=null; //q1=undefined
四、函數的四種調用方式
- 在`ES6之前`,函數內部的this是由該函數的調用方式決定的
- 在ES6的箭頭函數之前的時代,想要判斷一個函數內部的this指向誰,就是根據下面的四種方式來決定的
-
函數內部的this跟大小寫、書寫位置無關
(一)、函數調用
- 函數內部的this指向window
例一:
var age=18; var p={ age:15, say:function(){ console.log(this.age); //18 } } var f1=p.say; //f1是函數 f1(); //函數調用-->this:window -->this.age=18
例二:
function Person(name) { this.name = name; } Person.prototype = { constructor: Person, say: function () { console.log(this.name); //abc } } //函數的第一種調用方式:函數調用 // -->函數內部的this指向window // -->window對象中的方法都是全局函數,window對象中的屬性都是全局變量 Person("abc"); var p1=Person.prototype.say p1()
例三:
function fn() { console.log(this.age) //undefined } fn();
(二)、方法調用
- 函數內部的this指向調用該方法的對象
例一:
function Person() { this.age = 20; } Person.prototype.run = function () { console.log(this.age); } var p1 = new Person(); p1.run(); //打印結果:20
例二:
var p2={ height:180, travel:function(){ console.log(this.height); } } p2.travel() //打印結果:180
例三:
var clear=function(){ console.log(this.length); } var length=50; var tom={ c:clear, length:100 }; tom.c(); //這里是方法調用的方式 //打印this.length 是50 還是100? //-->相當於:this是指向window還是指向tom呢? // -->結果為:100 // -->this:tom //結論:由於clear函數被當成tom.c()這種方法的形式來進行調用,所以函數內部的this指向調用該方法的對象:tom var tony={ d:clear,length:30 }; tony.d(); //方法調用的方式,所以clear函數內部的this指向tony的,
(三)、構造函數的調用方式(new調用)
- this指向構造函數的實例
例一:
function fn(name){ this.name=name; } //通過new關鍵字來調用的,那么這種方式就是構造函數的構造函數的調用方式,那么函數內部的this就是該構造函數的實例 var _n=new fn("小明"); //_n有個name屬性,值為:小明
例二:
function jQuery(){ var _init=jQuery.prototype.init; //_init就是一個構造函數 return new _init(); } jQuery.prototype={ constructor:jQuery, length:100, init:function(){ //this可以訪問到實例本身的屬性,也可以訪問到init.prototype中的屬性 //這里的init.prototype並不是jQuery.prototype console.log(this.length); //正確答案:undefined //變量聲明了未賦值才是undefind、屬性不存在也是undefined } }
例三:
function jQuery(){ var _init=jQuery.prototype.init; //_init就是一個構造函數 return new _init(); } jQuery.prototype={ constructor:jQuery, length:100, init:function(){ //this指向init構造函數的實例 //-->1、首先查看本身有沒有length屬性 //-->2、如果本身沒有該屬性,那么去它的原型對象中查找 //-->3、如果原型對象中沒有,那么就去原型對象的原型對象中查找,最終一直找到根對象(Object.prototype) //-->4、最終都沒有找到的話,我們認為該對象並沒有該屬性,如果獲取該屬性的值:undefined console.log(this.length); //100 } } var $init=jQuery.prototype.init; //修改了init函數的默認原型,指向新原型 $init.prototype=jQuery.prototype; jQuery();
(四)、上下文調用方式(call、apply、bind)
-
call方法
call方法的第一個參數:
1、如果是一個對象類型,那么函數內部的this指向該對象
2、如果是undefined、null,那么函數內部的this指向window
3、如果是數字-->this:對應的Number構造函數的實例
--> 1 --> new Number(1)
4、如果是字符串-->this:String構造函數的實例
--> "abc" --> new String("abc")
5、如果是布爾值-->this:Boolean構造函數的實例
--> false --> new Boolean(false)
function f1(){ console.log(this); } //call方法的第一個參數決定了函數內部的this的值 f1.call([1,3,5]) f1.call({age:20,height:1000}) f1.call(1) f1.call("abc") f1.call(true); f1.call(null) f1.call(undefined);
-
apply方法
1、上述call的代碼apply完全可以替換
2、call和apply都可以改變函數內部的this的值
3、不同的地方:傳參的形式不同
function toString(a,b,c){ console.log(a+" "+b+" "+c); } toString.call(null,1,3,5) //"1 3 5" toString.apply(null,[1,3,5])//"1 3 5"
-
bind方法
bind是es5中才有的(IE9+)
例一:
var obj = { age:18, run : function(){ console.log(this); //this:obj var _that=this; setTimeout(function(){ //this指向window console.log(this.age); console.log(_that.age); //undefined是正確的 },50); } }
例二:
var obj5 = { age:18, run : function(){ console.log(this); //this:obj5 setTimeout((function(){ console.log(this.age); }).bind(this),50); //this:obj5 //通過執行了bind方法,匿名函數本身並沒有執行,只是改變了該函數內部的this的值,指向obj5 } } obj5.run();
例三:
//bind基本用法 function speed(){ console.log(this.seconds); } //執行了bind方法之后,產生了一個新函數,這個新函數里面的邏輯和原來還是一樣的,唯一的不同是this指向{ seconds:100 } var speedBind = speed.bind({ seconds:100 }); speedBind(); //100
例四:
(function eat(){ console.log(this.seconds); }).bind({ seconds:360 })() //360
例五:
var obj={ name:"西瓜", drink:(function(){ //this指向了:{ name:"橙汁" } console.log(this.name); }).bind({ name:"橙汁" }) } obj.drink(); //"橙汁"