繼承
一、混入式繼承
for in
使用for in遍歷對象1的屬性,將所有的屬性添加到另外一個對象2上
這時候就可以稱 對象2 繼承自 對象1
二、原型繼承
- 利用原型中的成員可以被和其相關的對象共享這一特性,可以實現繼承,這種實現繼承的方式就是原型繼承:
- 一、利用對象的動態特性,為原型對象添加成員,不是嚴格意義上的繼承;如下例中的p對象繼承自原型對象。
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){
console.log("我想死你啦");
}
var p = new Person("馮鞏",50);
p.sayHello();
- 二、直接替換原型對象(原型的使用方式):
- 有風險,在替換之后,原有的成員都會丟失;
- 替換原型對象的時候,需要手動去指定原型對象的construtor屬性;
function Person(name,age){
this.name = name;
this.age = age;
}
var parent = {
sayHello:function(){
console.log("我想死你啦");
}
}
//讓p繼承自parent,替換原型對象即可
Person.prototype = parent;
var p = new Person("馮鞏",50);
p.sayHello();
- 三、利用混入給原型對象添加成員
function Person(name,age){
this.name = name;
this.age = age;
}
var parent = {
sayHello:function(){
console.log("朋友們還好嗎?");
}
}
//混入式繼承
for(k in parent){
Person.prototype[k] = parent[k];
}
var p = new Person("馮鞏",50);
p.sayHello();
繼承的應用
- 對內置對象進行擴展:
var arr = [1,2,3];
Array.prototype.sayHello = function(){
console.log("我是數組);
}
arr.sayHello();
- 但是,如果直接修改內置對象的原型,會影響到整個開發團隊。
- 如何安全的擴展一個內置對象:
function MyArray(){
//添加自己的屬性
this.name = "我是一個數組";
this.sayHello = function(){
console.log("你好,我是數組");
}
}
var arr = new Array();
//繼承之后(替換原型,我的數組中就有了原生數組的所有屬性和方法)
MyArray.prototype = arr;
var myArr = new MyArray();
三、經典繼承
var 對象1 = Object.create(對象2);
這個時候,創建出來的對象1繼承自對象2
Object.create方法是ES5中提出來的,存在兼容性問題
如何解決?
1.檢測瀏覽器是否支持Object.create方法,如果不支持,直接手動給Object添加create方法
2.自定義函數,在函數內部判斷瀏覽器是否支持Object.create方法,如果不支持,則手動創建對象返回,否則直接調用
function create(obj){
if(Object.create){
return Object.create(obj);
}else{
function F(){
}
F.prototype = obj;
return new F();
}
}
原型鏈
什么是原型鏈
每個構造函數都有原型對象,每個對象都有構造函數,每個構造函數的原型對象都是對象,那么這個原型對象也會有構造函數,那么這個原型對象的構造函數也會有原型對象,這樣就會形成一個鏈式的結構,我們稱之為原型鏈。
原型結構的基本形式
function Person(){}
var p = new Person();
p ---> Person.prototype ---> Object.prototype ---> null
- 屬性搜索原則:
- 當訪問一個對象的成員的時候,會先在自身查找,如果找到直接使用;
- 如果沒有找到,就去當前對象的原型對象中去查找,如果找到就直接使用;
- 如果沒有找到,繼續在原型對象的原型對象中查找,如果找到了就直接使用;
- 如果還沒有找到,就繼續向上查找,直到查找到Object.prototype,如果還沒有找到:是屬性的話就返回undefined,是方法的話就報錯。
原型繼承是什么?
通過修改原型鏈的結構,實現的繼承方式就是原型繼承
對象和原型的成員關系
function Person(){};
var p = new Person();
- p對象中包含的成員有:Person.prototype中的成員和自身擁有成員;
- Person.prototype中的成員有:Object.prototype的成員和自身的成員
- p對象可以訪問Person.prototype和Object.prototype中的所有成員
Object.prototype的成員
- constructor:指向和該原型相關的構造函數;
- hasOwnProperty方法:判斷對象本身是否擁有某個屬性;
- isPrototypeOf方法:判斷是否是對象的原型對象;
- properIsEnumerable方法:1.判斷屬性是否屬于對象本身,2.判斷屬性是否可以被遍歷;返回的結果為:1&&2
- toString:將對象轉換成字符串; toLocalString轉換成字符串的時候應用的本地的設置格式;
- valueOf 方法:在對象參與運算的時候,首先調用valueOf方法獲取對象的值,如果該值無法參與運算,將會調用toString方法;
-
__proto__
屬性:指向當前對象的原型對象的屬性,可以使用對象.__proto__
去訪問對象的原型對象。
Function
3種創建函數的方式
* 直接聲明函數:function fn(){}
* 函數表達式:var fn = function(){}
* new Function:var fn = new Function();
可以用Function來創建函數:
語法:
//創建一個空的函數
var 函數名 = new Function();
//創建一個沒有參數的函數
var 函數名 = new Function("函數體")
//當給Fucntion傳多個參數的時候,最后一個參數為函數體,前面的參數為創建出來的函數的形參
var 函數名 = new Function("參數1","參數2", "參數3",..."函數體")
//Function接收的所有的參數都是字符串類型的!!!
- 創建一個沒有參數的函數:
var fn = new Function("console.log('我可是函數體喲!!!')");
fn();
- 創建一個帶有參數的函數:
var fn = new Function("a","b","console.log(a+b);");
fn(2,3); //5
arguments對象
- arguments對象是函數內部的一個對象,在函數調用的時候,系統會默認的將所有傳入的實參存入該對象;
注意:不管有沒有形參,實參都會被存入該對象。
是一個偽數組,arguments.length可以用來表示傳入的實參的個數;
arguments.callee,指向函數本身。
例. 不管輸入多少個數,總是輸出最大的數:
直接聲明函數:
function max() {
var maxValue = arguments[0];
for(var i=1; i<arguments.length; i++){
if (maxValue < arguments[i]){
maxValue = arguments[i];
}
}
return maxValue;
};
console.log(max(1, 3, 7, 9, 5));
- 函數表達式:
var max = function() {
var maxValue = arguments[0];
for(var i=1; i<arguments.length; i++){
if (maxValue < arguments[i]){
maxValue = arguments[i];
}
}
return maxValue;
};
console.log(max(1, 3, 7, 9, 5));
- new Function:
var max = new Function("a","b","var maxValue=arguments[0];" +
"for(i=1;i<arguments.length;i++){" +
"if(maxValue<arguments[i]){" +
"maxValue=arguments[i];" +
"}}" +
"return maxValue;"
);
console.log(max(1,2,3,4,59));
eval
可以將字符串轉換成js代碼并執行,但是不推薦使用,存在安全性問題。
var str = "var a = 10;";
eval(str);
console.log(a); //10
- 注意:當使用eval解析JSON格式字符串的時候,要注意,會將{}解析為代碼塊
- 1.可以在JSON格式字符串前面拼接
"var 變量名 ="
:
var obj = "{name:'yijiang',age:10};";
eval("var o = "+obj);
console.log(o);
- 2.用
()
把JSON格式字符串括起來,eval("("+JSON格式的字符串+")")
:
Function和eval的區別:
- 共同點:都可以將字符串轉換成js代碼;
- 不同點:
- Function創建出來的是函數,并不會直接調用,只用手動去調用才會執行;
- eval把字符串轉成代碼之后,直接就執行了。
靜態成員和實例成員
靜態成員
是指構造函數的屬性和方法。通過構造函數去訪問的屬性和方法就是靜態成員
實例成員
是指實例的屬性和方法。通過對象(實例)去訪問的屬性和方法就是實例成員
function Person(){
this.age = 28;
this.brother = "yijiang";
}
Person.prototype = {};
//其中age、brother是實例成員;
//prototype是靜態成員
//__proto__是實例成員
通常情況下:
- 把工具方法作為靜態成員;
- 把跟對象相關的方法作為實例成員。
補充:
instanceof關鍵字:
對象 instanceof 構造函數;
:判斷該構造函數的原型是否存在于該對象的原型鏈上。Function的原型鏈:
Function也可以被當做一個構造函數;
通過Function new出來的函數可以被當做是實例化的對象;
那么Function這個構造函數也有原型對象,Function的原型對象是一個空的函數;
Function的原型對象的原型對象是Object.prototype。
- Object構造函數 是通過 Function構造函數 實例化出來的;
- Function構造函數 也是通過 Function構造函數 實例化出來的(不要強行去理解,知道就好)
面向對象總復習
1.什么叫面向對象
-
面向對象是一種思想
- 把解決問題的關注點放到解決問題所需要的一系列對象上
-
面向過程是一種思想
- 把解決問題的關注點放到解決問題的每一個詳細步驟上
2.面向對象的三大特性
封裝
繼承
多態
3.什么是對象
萬物接對象
4.什么是js對象
鍵值對兒的集合(無序)
5.名詞提煉法
一句話中的所有的名詞都可以被當做對象
6.如何用js對象模擬現實生活中的對象
屬性對應特征
方法對應行為
7.創建對象的方式
使用對象字面量
使用內置的構造函數Object
使用簡單工廠函數(不推薦使用)
自定義構造函數
8.傳統構造函數中存在的問題
如果把方法定義在構造函數中,每創建一個對象,都會新建一個方法,這樣同樣的代碼會在內存中存在多分,造成資源浪費
9.如何解決問題8
把方法提取出來定義在全局中,在構造函數中引用該函數
使用原型來解決,原型中的所有成員都可以被所有跟其關聯的對象訪問
10.原型是什么
在構造函數創建的時候,系統默認的會為這個構造函數創建并關聯一個對象,這個對象就是原型對象
這個原型對象默認是一個空的對象,該對象中的所有成員可以被所有通過該構造函數實例化出來的對象訪問
11.原型的作用
該對象中的所有成員可以被所有通過該構造函數實例化出來的對象訪問
12.原型的使用方式(實現原型繼承的方式)
1.利用對象的動態特性給原型對象添加成員
2.直接替換原型對象
3.通過混入的方式給原型對象添加成員
13.原型鏈
每一個構造函數都有原型對象,每一個原型對象都有構造函數,這樣就形成一個鏈式的結構,稱之為原型鏈
14.繼承的實現方式
1.混入式繼承 for-in
2.原型繼承 通過更改原型鏈的結構,實現的繼承,就是原型繼承
3.經典繼承 Object.creat() 有兼容性問題
//var 對象名 = Object.create(要繼承的對象)
15.Object.prototype的成員
constructor 屬性 指向該原型相關的構造函數
hasOwnProperty 方法 判斷對象本身是否擁有某個屬性 obj.hasOwnProperty("屬性名")
isPrototypeOf 方法 判斷一個對象是不是另一個對象的原型對象 obj1.isPrototypeOf(obj2)
propertyIsEnumerable 方法 先判斷屬性是否屬于對象本身,如果不是,返回false,如果是,就繼續判斷屬性是否可以被遍歷,如果是才返回ture 反之則false
toString toLocaleString 方法 轉換成字符串 toLocaleString轉換成本地格式的字符串
valueOf 方法 當對象參加運算的時候,會首先調用valueOf方法獲取對象的值,如果獲取的值不能參與運算,則調用toString方法
proto 屬性 指向對象關聯的原型對象
16.Function eval
都可以將字符串轉換成代碼
不同點
Function 創建出來的是函數,不會直接調用,除非手動調用
eval 直接可以將字符串轉換成代碼,并執行
17.arguments
函數內部的一個對象,在函數調用的時候,系統會默認的將所有傳入的實參存入該對象
arguments.length 表示傳入實參的個數
arguments.callee 指向當前函數 (匿名函數中使用,因為他沒有名字)
案例:歌曲管理器
- 在當前對象的方法中,調用當前對象中的另一個方法,需要使用this
遞歸
什么是遞歸
在程序中,所謂的遞歸,就是函數自己直接或間接的調用自己。調用自己分兩種:
直接調用自己;
間接調用自己。
就遞歸而言最重要的就是
跳出結構,
因為跳出了才可以有結果。
化歸思想
化歸思想:將一個問題由難化易,由繁化簡,由復雜化簡單的過程稱為化歸,它是轉化和歸結的簡稱。
遞歸思想就是將一個問題轉換為一個已解決的問題來實現的。
假如有一個函數f,如果它是遞歸函數的話,,那么也就是說函數體內的問題還是轉換為f的形式。
function f() {
... f( ... ) ...
}
例子:
求1,2,3,4,5...100的和。
- 首先假定遞歸函數已經寫好,假設是foo。即foo(100)就是求1到100的和;
- 尋找遞推關系,就是n與n-1,或n-2之間的關系:
foo(n) == n + foo( n - 1 )
var res = foo(100);
var res = foo(99) + 100;
- 將遞推結構轉換為遞歸體
function foo(n){
return n + foo( n - 1 );
}
上面就是利用了化歸思想:
將求 100 轉換為 求 99;
將求 99 轉換為 求 98;
...
將求 2 轉換為 求 1;
求 1 結果就是 1;
即: foo( 1 ) 是 1。
將臨界條件加到遞歸體中(求1的結果為1)
function foo( n ) {
if ( n == 1 ) return 1;
return n + foo( n - 1 );
}
練習:
一、求1,3,5,7,9,...第n項的結果與前n項和。序號從0開始
- 先求第n項:
- 首先假定遞歸函數已經寫好,假設是fn。 那么第n項就是fn(n)
- 找遞推關系:fn(n) == f(n-1) + 2
- 遞歸體:
function fn(n) {
return fn(n-1) + 2;
}
- 找臨界條件
求 n -> n-1
求 n-1 -> n-2
...
求 1 -> 0
求 第 0 項, 就是 1 - 加入臨界條件:
function fn( n ) {
if ( n == 0 ) return 1;
return fn( n-1 ) + 2;
}
- 再看求前n項和
- 假設已完成:sum( n ) 就是前 n 項和;
- 找遞推關系:前n項和等于第n項 + 前n-1項的和;
- 遞歸體
function sum( n ) {
return fn( n ) + sum( n - 1 );
}
- 找臨界條件:
- n == 1結果為 1;
- 加入臨界條件:
function sum( n ) {
if (n == 0) return 1;
return fn(n) + sum(n - 1);
}
二、Fibonacci數列:1,1,2,3,5,8,13,21,34,55,89...求其第n項。
- 遞推關系:fn(n) == fn(n-1) + fn(n - 2)
function fib( n ) {
if ( n == 0 || n == 1 ) return 1;
return fib( n - 1 ) + fib( n - 2 );
}
三、階乘:一個數字的階乘表示的是從 1 開始累乘到這個數字。例如:3!表示123。規定 0 沒有階乘, 階乘從1開始。
- 求n的階乘
function foo ( n ) {
if ( n == 1 ) return 1;
return foo( n - 1 ) * n;
}
案例:使用遞歸遍歷所有的后代元素:
- DOM沒有提供直接獲取后代元素的API;
- 但是可以通過childNode來獲取所有的子元素;
作用域
域,表示的是一個范圍,作用域,就是作用范圍。
作用域說明的是一個變量可以在什么地方被使用,什么地方不能被使用。
塊級作用域
- JavaScript中沒有塊級作用域
{
var num = 123;
{
console.log( num );
}
}
console.log( num );
- 上面這段代碼在JavaScript中是不會報錯的,但是在其他的編程語言中(C#、C、JAVA)會報錯。
- 這是因為,在JavaScript中沒有塊級作用域,使用{}標記出來的代碼塊中聲明的變量num,是可以被{}外面訪問到的。
- 但是在其他的編程語言中,有塊級作用域,那么{}中聲明的變量num,是不能在代碼塊外部訪問的,所以報錯。
詞法作用域
什么是詞法作用域?
詞法( 代碼 )作用域,就是代碼在編寫過程中體現出來的作用范圍。代碼一旦寫好,不用執行, 作用范圍就已經確定好了, 這個就是所謂詞法作用域。
在 js 中詞法作用域規則:
函數允許訪問函數外的數據;
整個代碼結構中只有函數可以限定作用域;
作用域規則首先使用提升規則分析;
如果當前作用規則中有名字了,就不考慮外面的名字。
案例1:
var num = 123;
function foo() {
console.log( num );
}
foo();
- 案例2:
if ( false ) {
var num = 123;
}
console.log( num ); // undefiend
- 例子3:
var num = 123;
function foo() {
var num = 456;
function func() {
console.log( num );
}
func();
}
foo(); //456
- 例子4:
var num1 = 123;
function foo1() {
var num1 = 456;
function foo2() {
num1 = 789;
function foo3 () {
console.log( num1 );
}
foo3();
console.log( num1 );
}
foo2();
console.log( num1 );
}
foo1();
//789
//789
//789
- 面試題
var num = 123;
function func1(){
console.log(num);
}
function func2(){
var num = 456;
func1();
}
func2(); //123[詞法作用域]
變量提升
JavaScript是解釋型的語言,但是他并不是真的在運行的時候逐句的往下解析執行。
我們來看下面這個例子:
func();
function func(){
alert("Funciton has been called");
}
在上面這段代碼中,函數func的調用是在其聲明之前,如果說JavaScript代碼真的是逐句的解析執行,那么在第一句調用的時候就會出錯,然而事實并非如此,上面的代碼可以正常執行,并且alert出來Function has been called。
所以,可以得出結論,JavaScript并非僅在運行時簡簡單單的逐句解析執行!
JavaScript 預解析
JavaScript引擎在對JavaScript代碼進行解釋執行之前,會對JavaScript代碼進行預解析,在預解析階段,會將以關鍵字var和function開頭的語句塊提前進行處理。
關鍵問題是怎么處理呢?
當變量和函數的聲明處在作用域比較靠后的位置的時候,變量和函數的聲明會被提升到作用域的開頭。
重新來看上面的那段代碼
func();
function func(){
alert("Funciton has been called");
}
- 由于JavaScript的預解析機制,上面的代碼就等效于:
function func(){
alert("Funciton has been called");
}
func();
- 看完函數聲明的提升,再來看一個變量聲明提升的例子:
alert(a);
var a = 1;
- 由于JavaScript的預解析機制,上面這段代碼,alert出來的值是undefined,如果沒有預解析,代碼應該會直接報錯a is not defined,而不是輸出值。
- Wait a minute,不是說要提前的嗎?那不是應該alert出來1,為什么是undefined?
- 那么在這里有必要說一下聲明、定義、初始化的區別。其實這幾個概念是C系語言的人應該都比較了解的。
行為 | 說明 |
---|---|
聲明 | 告訴編譯器/解析器有這個變量存在,這個行為是不分配內存空間的,在JavaScript中,聲明一個變量的操作為:var a; |
定義 | 為變量分配內存空間,在C語言中,一般聲明就包含了定義,比如:int a; ,但是在JavaScript中,var a; 這種形式就只是聲明了。 |
初始化 | 在定義變量之后,系統為變量分配的空間內存儲的值是不確定的,所以需要對這個空間進行初始化,以確保程序的安全性和確定性 |
賦值 | 賦值就是變量在分配空間之后的某個時間里,對變量的值進行的刷新操作(修改存儲空間內的數據) |
所以我們說的提升,是聲明的提升。 |
- 那么再回過頭看,上面的代碼就等效于:
var a; //這里是聲明
alert(a);//變量聲明之后并未有初始化和賦值操作,所以這里是 undefined
a = 1;
復雜點的情況分析
- 函數同名,觀察下面這段代碼:
func1();
function func1(){
console.log('This is func1');
}
func1();
function func1(){
console.log('This is last func1');
}
- 輸出結果為:
This is last func1
This is last func1
- 原因分析:由于預解析機制,func1的聲明會被提升,提升之后的代碼為:
function func1(){
console.log('This is func1');
}
function func1(){
console.log('This is last func1');
}
func1();
func1();
同名的函數,后面的會覆蓋前面的,所以兩次輸出結果都是This is last func1。
變量和函數同名
alert(foo);
function foo(){}
var foo = 2;
- 當出現變量聲明和函數同名的時候,只會對函數聲明進行提升,變量會被忽略。所以上面的代碼的輸出結果為:
function foo(){}
- 解析之后的代碼:
function foo(){};
alert(foo);
foo = 2;
- 再來看一種
var num = 1;
function num () {
alert( num );
}
num();
- 代碼執行結果為:
Uncaught TypeError: num is not a function
- 直接上預解析后的代碼:
function num(){
alert(num);
}
num = 1;
num();
預解析是分作用域的
- 聲明提升并不是將所有的聲明都提升到window對象下面,提升原則是提升到變量運行的環境(作用域)中去。
function showMsg()
{
var msg = 'This is message';
}
alert(msg); // msg未定義
- 把預解析之后的代碼寫出來:
function showMsg()
{
var msg;
msg = 'This is message';
}
alert(msg); // msg未定義
- 分作用域:
var msg = "aaa";
function showMsg()
{
alert(msg); // msg未定義
var msg = 'This is message';
}
var aaa = 10;
function f1() {
console.log(aaa);
aaa = 20;
}
console.log(aaa);
f1();
console.log(aaa);
//10
//10
//20
var aaa = 10;
function f1() {
console.log(aaa);
var aaa = 20;
}
console.log(aaa);
f1();
console.log(aaa);
//10
//undefined
//10
預解析是分段的
- 分段,其實就分script標簽的
<script>
func(); // 輸出 AA2;
function func(){
console.log('AA1');
}
function func(){
console.log('AA2');
}
</script>
<script>
function func(){
console.log('AA3');
}
</script>
在上面代碼中,第一個script標簽中的兩個func進行了提升,第二個func覆蓋了第一個func,但是第二個script標簽中的func并沒有覆蓋上面的第二個func。所以說預解析是分段的。
tip
:但是要注意,分段只是單純的針對函數,變量并不會分段預解析。
函數表達式并不會被提升
func();
var func = function(){
alert("我被提升了");
};
- 這里會直接報錯,func is not a function,原因就是函數表達式,并不會被提升。只是簡單地當做變量聲明進行了處理,如下:
var func;
func();
func = function(){
alert("我被提升了");
}
練習:
if("a" in window){
var a = 10;
}
alert(a); //10
- 上述代碼預解析:
var a;
if("a" in window){
a = 10;
}
alert(a);
function fn(){
if("a" in window){
var a = 10;
}
alert(a);
}
fn(); //undefined
var foo = 1;
function bar(){
if(!foo){
var foo = 10;
}
alert(foo); //10
}
bar();
條件式函數聲明
console.log(typeof func);
if(true){
function(){
return 1;
}
}
console.log(typeof func);
上面這段代碼,就是所謂的條件式函數聲明,這段代碼在Gecko引擎中打印"undefined"、"function";而在其他瀏覽器中則打印"function"、"function"。
原因在于Gecko加入了ECMAScript以外的一個feature:條件式函數聲明。
說明:
Conditionally created functions Functions can be conditionally declared, that is, a function declaration can be nested within an if statement.
Note: Although this kind of function looks like a function declaration, it is actually an expression (or statement), since it is nested within another statement. See differences between function declarations and function expressions.
Note中的文字說明,條件式函數聲明的處理和函數表達式的處理方式一樣,所以條件式函數聲明沒有聲明提升的特性。
復習:
作用域:變量的作用范圍;
js中的作用域是詞法作用域:代碼在寫好之后,變量的作用域已經確定;
js中沒有塊級作用域。
js中只有函數可以創建作用域;
變量提升:在分析代碼的時候,首先將以var聲明的變量和function聲明的函數進行提升;再去執行代碼的具體執行過程:
變量的提升是分作用域的;
當函數和變量名相同的時候,只提升函數,不提升變量;
函數名相同,全部都會被提升,后面的函數會覆蓋前面的函數;
函數表達式中函數不會被提升,但是變量會被提升。
func();
var func = function () {
console.log(11111);
}
- 上述代碼執行會報錯,變量提升如下:
var func;
func();
func = function () {
console.log(11111);
}
- 如下代碼就不會報錯:
var func = function () {
console.log(11111);
};
func();
- 并不是函數內部寫了變量,這個變量就屬于這個函數的作用域,而是必須使用var來聲明的變量才屬于這個函數作用域。
作用域鏈
什么是作用域鏈
只有函數可以制造作用域結構,那么只要是代碼,就至少有一個作用域,即全局作用域。
凡是代碼中有函數,那么這個函數就構成另一個作用域。如果函數中還有函數,那么在這個作用域中就又可以誕生一個作用域。
將這樣的所有的作用域列出來,可以有一個結構:函數內指向函數外的鏈式結構。就稱作作用域鏈。
函數內部的作用域可以訪問函數外部的作用域;
如果有多個函數嵌套,那么就會構成作用域鏈。
- 例如:
function f1() {
function f2() {
}
}
//f2-->f1-->全局
var num = 456;
function f3() {
function f4() {
}
}
//f4-->f3-->全局
- 繪制作用域鏈的步驟:
- 看整個全局是一條鏈,即頂級鏈,記為 0 級鏈;
- 看全局作用域中,有什么成員聲明,就以方格的形式繪制到 0 級練上;
- 再找函數,只有函數可以限制作用域,因此從函數中引入新鏈,標記為 1 級鏈;
- 然后在每一個 1 級鏈中再次往復剛才的行為。
變量的訪問規則
首先看變量在第幾條鏈上;在當前鏈上看是否有變量的定義與賦值,如果有直接使用;
如果沒有到上一級鏈上找( n - 1 級鏈 ), 如果有直接用,停止繼續查找;
如果還沒有再次往上剛找... 直到全局鏈( 0 級 ),還沒有就是 is not defined。
注意,同級的鏈不可混合查找。
練習1:繪制作用域鏈
function f1() {
var num = 123;
function f2() {
console.log( num );
}
f2();
}
var num = 456;
f1(); //123
- 練習2:
var num = 456;
function f() {
num = 678;
function foo() {
var num = 999;
console.log(num);
}
foo();
console.log(num);
}
f(); //999 678
- 練習3:變量提升會提升到函數前面
function fff() {
console.log(num);
}
fff(); //undefined
var num = 123;
function fff() {
console.log(num);
}
var num = 123;
fff(); //123
如何分析代碼
- 在分析代碼的時候切記從代碼的運行進度上來分析,如果代碼給變量賦值了,一定要標記到圖中;
- 如果代碼比較復雜,可以在圖中描述代碼的內容,有事甚至需要將原型圖與作用域圖合并分析。
練習
- 第一題:
var num = 123;
function f1() {
console.log( num );
}
function f2() {
var num = 456;
f1();
}
f2(); //123
- 第二題:
var num = 123;
function f1() {
console.log( num );
}
function f2() {
num = 456;
f1();
}
f2(); //456
補充
聲明變量使用
var
,如果不使用var
聲明的變量就是全局變量(禁用);因為在任何代碼結構中都可以使用該語法。 那么再代碼維護的時候會有問題,所以除非特殊原因不要這么用。
下面的代碼的錯誤
function foo () {
var i1 = 1 // 局部
i2 = 2, // 全局
i3 = 3; // 全局
}
- 此時注意:
var arr = [];
for ( var i = 0; i < 10; i++ ) {
arr.push( i );
}
for ( var i = 0; i < 10; i++ ) {
console.log( arr[ i ] );
} //0 1 2 3 4 5 6 7 8 9
// 一般都是將變量的聲明全部放到開始的位置,避免出現因為提升而造成的錯誤
var arr = [],
i = 0;
for ( ; i < 10; i++ ) {
arr.push( i );
}
for ( i = 0; i < 10; i++ ) {
console.log( arr[ i ] );
} //0 1 2 3 4 5 6 7 8 9
閉包
閉包的概念
- 閉包從字面意思理解就是閉合,包起來。
- 簡單的來說閉包就是,一個具有封閉的對外不公開的包裹結構或空間。
- 在JavaScript中函數可以構成閉包。一般函數是一個代碼結構的封閉結構,即包裹的特性,同時根據作用域規則,只允許函數訪問外部的數據,外部無法訪問函數內部的數據,即封閉的對外不公開的特性。因此說函數可以構成閉包。
閉包要解決什么問題?
- 閉包內的數據不允許外界訪問;
- 要解決的問題就是間接訪問該數據。
訪問數據的問題
- 我們觀察下面的函數foo,在foo內部有一個變量num,能否在函數外部訪問到這個變量num呢?
function foo () {
var num = 123;
return num;
}
var res = foo();
console.log( res ); // => 123
分析:
在上面的代碼中,確實可以訪問到num這個函數內部的變量。但是能不能多次訪問呢?
不能,因為每次訪問都得重新調用一次foo函數,每次調用都會重新創建一個num = 123,然后返回。
解決思路
函數內的數據不能直接在函數外被訪問,是因為作用域的關系,上級作用域不能直接訪問下級作用域中的數據。
但是如果反過來,下級作用域可以直接訪問上級作用域中的數據。那么如果在函數foo內定義一個函數,那么在這個內部函數中是可以直接訪問foo中的num的。
function foo() {
var num = Math.random();
function func() {
return num;
}
return func;
}
var f = foo();
// f可以直接訪問num,而且多次訪問,訪問的也是同一個,并不會返回新的num
var res1 = f();
var res2 = f();
如何獲得超過一個數據
- 函數的返回值只能有一個,那按照上面的方法,我們只能對函數內部的一個數據進行操作。怎么操作函數內的多個數據呢?
- 可以使用對象,代碼如下:
function foo () {
var num1 = Math.random();
var num2 = Math.random();
//可以將多個函數包含在一個對象內進行返回,這樣就能在函數外部操作當前函數內的多個變量
return {
num1: function () {
return num1;
},
num2: function () {
return num2;
}
}
}
如何完成讀取一個數據和修改這個數據
- 前面講的都是如何去獲取函數內部的數據,接下來我們考慮如何修改函數內部的數據。
- 同樣,也是使用內部的函數進行操作。
function foo() {
var num = Math.random();
//分別定義get和set函數,使用對象進行返回
return {
//get_num負責獲取數據
get_num: function() {
return num;
},
//set_num負責設置數據
set_num: function(value) {
num = value;
}
}
}
閉包的基本結構
- 一般閉包要解決的的問題就是要想辦法間接的獲得函數內數據的使用權。那么我們就可以總結出一個基本的使用模型:
- 寫一個函數,函數內定義一個新函數,返回新函數,用新函數獲得函數內的數據;
- 寫一個函數,函數內定義一個對象,對象中綁定多個函數( 方法 ),返回對象,利用對象的方法訪問函數內的數據。
閉包的作用
閉包的基本作用:可以通過閉包返回的函數或者方法,來修改函數內部的數據。
- 在函數外部想要修改數據,只能通過函數內部的方法;
- 我們可以在函數內部定義的這個方法里,設置安全措施,比如檢驗之類的操作,可以保證系統的安全性和穩定性。
復習
使用遞歸獲取后代元素
作用域
變量起作用的范圍;
什么是塊級作用域:
JS中沒有塊級作用域,使用代碼塊限定的作用域就是塊級作用域。
JS中的作用域叫做詞法作用域:
在代碼寫好的時候,就能確定變量的作用域叫詞法作用域。
動態作用域(是詞法作用域就不可能是動態作用域)。
在JS中,只有函數能創造作用域。
變量提升
JS代碼的運行分兩個階段:
預解析階段:變量名和函數名提升(將var聲明的變量和function聲明的函數提升到當前作用域的最上方);
執行階段。
注:
變量名和函數名相同的時候,只提升函數名,不提升變量名;
函數名相同的時候,都提升,但是后面的函數會覆蓋前面的函數;
函數表達式,只會提升變量名,不會提升后面的函數;
變量提升只會將變量和函數提升到當前作用域的最上方。
變量提升是分塊
<script></script>
的。條件式函數聲明是否會被提升,取決于瀏覽器,不推薦去寫
foo(); //報錯
if(true){
function foo(){
console.log("123");
}
}
foo(); //123
作用域鏈
- 只要是函數都有作用域,函數內部的作用域都可以訪問函數外部的作用域,當多個函數嵌套的時候,就會形成一個鏈式的結構,這個結構就是作用域鏈。
繪制作用域鏈圖的步驟
- 先繪制0級作用域鏈;
- 在全局作用域中查找變量和函數的聲明,找到之后將所有的變量和函數用小方格放在0級作用域上;
- 在0級作用域鏈上的函數引出1級作用域鏈;
- 再去每一個1級作用域鏈中查找變量和函數的聲明,找到之后...
- 以此重復,就畫好了整個作用域鏈。
變量搜索規則
- 首先在訪問變量的作用域中查找該變量,如果找到就直接使用;
- 如果沒有找到,就去上一級作用域中繼續查找,如果找到就直接使用;
- 如果沒有找到,就繼續去上一級作用于中查找,知道找到0級為止;
- 如果找到了就用,如果沒有找到就undefined(變量)或者報錯(函數)。
閉包
- 閉包是一個封閉的對外不公開的包裹結構或者空間;
- JS中的閉包是函數;
- 閉包要解決的問題:在函數外部訪問不到函數內部的數據;要解決的問題就是需要在函數外部間接的訪問函數內部的數據。
閉包的基本結構
- 返回一個數據:
function outer(){
var data = "數據";
return function(){
return data;
}
}
- 返回多個數據:
function outer(){
var data1 = "數據1";
var data2 = "數據2";
return {
getData1:function(){
return data1;
},
setData1:function(value){
data1 = value;
return data1;
},
getData2:function(){
return data2;
},
setData2:function(value){
data2 = value;
return data2;
}
}
}
閉包的作用
- 如果把數據放在全局作用域內,那么所有人都可以隨意修改,這樣數據就不再可靠。
- 閉包可以創建一個私有的空間,在這個空間內部的數據,外部無法直接訪問;
- 外部空間想要訪問函數內部的數據,只能通過閉包提供的指定方法,在這個指定方法內部可以設置一些校驗規則,讓數據變得更加安全;
函數模式
特征:就是一個簡單的函數調用,函數名前面沒有任何的引導內容
function foo(){}
var func = function(){}
foo();
func();
(function(){})();
this在函數模式中的含義: this在函數中表示全局對象,在瀏覽器中是window對象
方法模式
特征: 方法一定是依附于一個對象, 將函數賦值給對象的一個屬性, 那么就成為了方法.
function f() {
this.method = function () {};
}
var o = {
method: function () {}
}
this在方法模式調用中的含義:表示函數所依附的這個對象
構造器調用模式
由于構造函數只是給 this 添加成員. 沒有做其他事情. 而方法也可以完成這個操作, 就 this 而言, 構造函數與方法沒有本質區別.
特征:使用 new 關鍵字, 來引導構造函數.
function Person(){
this.name = "zhangsan";
this.age = 19;
this.sayHello = function(){
};
}
var p = new Person();
構造函數中發this與方法中一樣, 表示對象, 但是構造函數中的對象是剛剛創建出來的對象
關于構造函數中return關鍵字的補充說明
構造函數中不需要return, 就會默認的return this
如果手動的添加return, 就相當于 return this
如果手動的添加return 基本類型; 無效, 還是保留原來 返回this
如果手動添加return null; 或return undefiend, 無效
如果手動添加return 對象類型; 那么原來創建的this就會被丟掉, 返回的是 return后面的對象
創建對象的模式
工廠方法
// 工廠就是用來生產的, 因此如果函數創建對象并返回, 就稱該函數為工廠函數
function createPerson( name, age, gender ) {
var o = {};
o.name = name;
o.age = age;
o.gender = gender;
return o;
}
// document.createElement()
構造方法
function Person(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
}
var p = new Person("zhangsan", 19, "男");
寄生式創建對象
function Person(name, age, gender){
var o = {};
o.name = name;
o.age = age;
o.gender = gender;
return o;
}
var p = new Person("Jack", 18, "male");
混合式創建
混合式繼承就是講所有的屬性放在構造方法里面,然后講所有的方法放在原型里面,使用構造方法和原型配合起來創建對象。
上下文調用模式
上下文(Context),就是函數調用所處的環境。
上下文調用,也就是自定義設置this的含義。
在其他三種調用模式中,函數/方法在調用的時候,this的值都是指定好了的,我們沒辦法自己進行設置,如果嘗試去給this賦值,會報錯。
上下文調用的語法
//第一種, apply
函數名.apply(對象, [參數]);
//第二種, call
函數名.call(對象, 參數);
//上面兩種方式的功能一模一樣,只是在傳遞參數的時候有差異。
功能描述:
語法中的函數名表示的就是函數本身,使用函數調用模式的時候,this默認是全局對象
語法中的函數名也可以是方法(如:obj.method),在使用方法模式調用的時候,this默認是指當前對象
在使用apply和call的時候,默認的this都會失效,this的值由apply和call的第一個參數決定
補充說明
如果函數或方法中沒有this的操作, 那么無論什么調用其實都一樣.
如果是函數調用foo(), 那么有點像foo.apply( window ).
如果是方法調用o.method(), 那么有點像o.method.apply( o ).
參數問題
call和apply在沒有后面的參數的情況下(函數無參數, 方法無參數) 是完全一樣的.
如下:
function foo() {
console.log( this );
}
foo.apply( obj );
foo.call( obj );
第一個參數的使用規則:
如果傳入的是一個對象, 那么就相當于設置該函數中的 this 為參數
如果不傳入參數, 或傳入 null. undefiend 等, 那么相當于 this 默認為 window
foo();
foo.apply();
foo.apply( null );
foo.call( undefined );
如果傳入的是基本類型, 那么 this 就是基本類型對應的包裝類型的引用
number -> Number
boolean -> Boolean
string -> String
第二個參數的使用規則
在使用上下文調用的時候, 原函數(方法)可能會帶有參數, 那么這個參數在上下文調用中使用第二個( 第 n 個 )參數來表示
function foo( num ) {
console.log( num );
}
foo.apply( null, [ 123 ] );
// 等價于
foo( 123 );
上下文調用模式的應用
上下文調用只是能修改this, 但是使用的最多的地方上是函數借用.
- 將偽數組轉換為數組
傳統的做法:
var a = {};
a[ 0 ] = 'a';
a[ 1 ] = 'b';
a.length = 2;
// 使用數組自帶的方法 concat
// 如果參數中有數組會把參數數組展開
// 語法: arr.concat( 1, 2, 3, [ 4, [ 5 ] ] );
// 特點:不修改原數組
var arr = [];
var newArr = arr.concat( a );
由于a是偽數組, 只是長得像數組. 所以上面的代碼不能成功,不能使用concat方法。
但是apply方法有一個特性, 可以將數組或偽數組作為參數。(IE8不支持偽數組操作)
foo.apply( obj, 偽數組 ); // IE8 不支持
利用apply方法,可以寫出以下
//將偽數組 a 作為 apply 的第二個參數
var newArr = Array.prototype.concat.apply( [], a )
處理數組轉換, 實際上就是將元素一個一個的取出來構成一個新數組, 凡是涉及到該操作的方法理論上都可以。
push方法
//用法:
arr.push( 1 ); //將這個元素加到數組中, 并返回所加元素的個數
arr.push( 1, 2, 3 ); //將這三個元素依次加到數組中, 返回所加個數
var a = { length: 0 }; // 偽數組
a[ a.length++ ] = 'abc'; // a[ 0 ] = 'abc'; a.length++;
a[ a.length++ ] = 'def';
// 使用一個空數組, 將元素一個個放到數組中即可
var arr = [];
arr.push( a ); // 此時不會將元素展開, 而是將這個偽數組作為一個元素加到數組中
// 再次利用 apply 可以展開偽數組的特征
arr.push.apply( arr, a );
// 利用 apply 可以展開偽數組的特性, 這里就相當于 arr.push( a[0], a[1] )
- 求數組中的最大值
傳統的做法
var max = arr[ 0 ];
for ( var i = 1; i < arr.length; i++ ) {
if ( arr[ i ] > max ) {
...
}
}
在 js 中的Math對象中提供了很多數學函數Math.max( 1,2,3 )
還是利用 apply 可以展開數組的特性
var arr = [ 123456,12345,1234,345345,234,5 ];
Math.max.apply( null, arr );
3.借用構造函數繼承
function Person ( name, age, gender ) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 需要提供一個 Student 的構造函數創建學生對象
// 學生也應該有 name, age, gender, 同時還需要有 course 課程
function Student ( name, age, gender, course ) {
Person.call( this, name, age, gender );
this.course = course;
}