您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關Javascript中面向對象和原型原型鏈是怎樣的,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
面向對象理論知識總述
* 核心答案 | 基礎知識要夯實
編程語言
1、OOP面向對象:
1)java,
2)python,
3)C++,
4)php,
5)C#(ASP.NET),
6)javascript -> Node.js
2、POP面向過程:
HTML和CSS是標記語言
1)less/sass/stylus:CSS預編譯語言,讓CSS具備面向對象編程的特點。
2)寫完的代碼無法被瀏覽器直接識別,需要編譯后(編譯成為正常的CSS)在瀏覽器中渲染。
什么是面向對象編程?
1、對象:泛指,萬物皆對象(JS中所有我們學習研究和開發的都是對象 「研究對象」);
2、類:對 “對象” 的一個細分,按照對應的功能特點,分成我們的大類和小類「類別」;
3、實例:某個類別中具體的事物;
* 關于類的“封裝、繼承、多態”
1)封裝:把實現某個功能的代碼封裝到函數中,起到“低耦合高內聚”的作用
2)繼承:子類及子類的實例繼承了父類中的屬性和方法
3)多態:函數的重載(方法名字相同,但是傳遞參數的個數或者類型不同,識別為兩個不同的方法 -> 后臺語言有這個特征,但是JS中不存在嚴格意義上的重載)和重寫(子類重寫父類的方法)
JS就是基于“面向對象”思想設計的編程語言
1、本身存在很多“內置類”
1)每一個數據類型都有一個自己所屬的內置類
2)獲取的元素集合或者節點集合也是有自己的類 HTMLCollection / NodeList
3)每一個元素標簽都有自己所屬的類
2、我們學習JS:拿出某個類的一個實例去研究和學習,當前實例研究明白后,那么當前實例所屬類下的其他實例,也具備這些特點...
自定義類的創建和一些細節知識
* 核心答案 | 基礎知識要夯實
自定義類(所有的類「內置類/自定義類」都是“函數數據類型”的值)
函數執行的時候基于new執行即可 “構造函數執行”。
? 例如一:普通函數 與 構造函數
function Fn(x, y) { let total = x + y; this.x = x; this.y = y; return total; } // 1、作為普通函數執行 Fn(10, 20); // 2、構造函數執行 // 說明:f1是Fn這個類的一個實例對象 let f1 = new Fn(10, 20); console.log(f1.x, f1.y, f1.total); // 說明:total只是上下文中的私有變量,和實例f1沒有關系」 // 結果:10 20 undefined
畫圖分析:( 有圖有真相 )
構造函數 VS 普通函數
1、構造函數執行,最開始會像普通函數執行一樣,形成私有的上下文。
1)AO;
2) SCOPE-CHAIN;
3)形參賦值;
4)變量提升;
5)代碼執行;
不同的地方:
1、創建上下文之后,瀏覽器默認幫助我們創建一個對象 “實例對象”。
1)把當前Fn函數當作一個類“構造函數”
2)創建的對象就是這個類的一個實例
2、初始this的時候,讓this指向當前創建的實例對象。
3、在代碼執行完,返回值的時候
1)如果函數沒有寫return,或者返回的是一個基本數據類型值,則瀏覽器默認,會把創建的實例對象返回;
2)如果函數本身返回的就是一個引用數據類型值,還是以自己返回的為主。
? 例如二:構造函數擴展
function Fn(x, y) { let total = x + y; this.x = x; this.y = y; return { name: '前端學苑' }; }
說明:
由于構造函數體中,默認自己返回一個引用類型值,所以f1不再是創建的Fn實例,而是自己返回的對象。
let f1 = new Fn(10, 20);
“實例 instanceof 構造函數” :檢測當前實例是否屬于這個類。
console.log(f1 instanceof Fn); //false
? 例如三:函數擴展
function Fn(x, y) { let total = x + y; this.x = x; this.y = y; this.say = function say() { console.log(`SAY:${total}`); }; } let f1 = new Fn(10, 20); let f2 = new Fn;
畫圖分析:( 有圖有真相 )
結果:
console.log(f1 === f2); //false console.log(f1.say === f2.say); //false
Fn VS Fn()
1) Fn代表的是函數本身(堆內存 -> ƒ Fn(x, y) {...});
2) Fn()是把函數執行,獲取其返回值;
new Fn VS new Fn()
都是把Fn執行了,只是第一個沒有傳遞實參,第二個可以傳遞實參而已。
1) new Fn; 運算優先級是18(無參數列表new)
2) new Fn(); 運算符優先級是19(有參數列表new)
* 檢測一個屬性是否為當前對象的成員
1)屬性名 in 對象:不論是私有屬性還是公有的屬性,只要有就是true;
2)對象.hasOwnProperty(屬性名):必須是對象的私有屬性,結果才是true;
說明:自己擴展一個方法 hasPubProperty(對象,屬性名):檢測當前屬性是否屬于對象的公有屬性(特點:必須有這個屬性,而且不是私有的)(需要擴展)
屬性和變量有什么共同點:
沒有。屬性是堆內存的成員,變量是棧內存或者上下文當中變量。
console.log('say' in f1); //true console.log('toString' in f1); //true console.log('total' in f1); //false console.log(f1.hasOwnProperty('say')); //true console.log(f1.hasOwnProperty('toString')); //false console.log(f1.hasOwnProperty('total')); //false
? 例如四:函數擴展
Object.prototype.AA = '前端學苑'; let obj = { name: 'xxx', age: 11, 0: 100, [Symbol('AA')]: 200, [Symbol.toPrimitive]: function () { return 0; } };
基于“for...in”循環遍歷對象
1) 優先遍歷數字屬性;
2) 不會遍歷到Symbol屬性;
3) 會把自己擴展到“類原型”上的公共屬性方法也遍歷到「可枚舉的」
for (let key in obj) {
// 在遍歷過程中,遍歷到公共屬性,則停止遍歷:
// 因為for...in遍歷的本意就是只遍歷私有的屬性即可
if (!obj.hasOwnProperty(key)) break;
console.log(key);
}
let keys = [ ...Object.keys(obj), ...Object.getOwnPropertySymbols(obj) ]; keys.forEach(key => { console.log(`屬性名:${String(key)},屬性值:${obj[key]}`); });
說明:
1)Object.keys(obj):獲取當前對象所有非Symbol的私有屬性「數組」 =>Object.getOwnPropertyNames.
2)Object.getOwnPropertySymbols(obj):獲取對象所有的Symbol私有屬性「數組」.
面向對象中的原型和原型鏈
* 核心答案 | 基礎知識要夯實
1、函數數據類型
1)普通函數
2)箭頭函數
3)生成器函數
4)構造函數(類)
2、對象數據類型
1)普通對象/數組對象/正則對象/日期對象...
2)實例也是對象數據類型的(排除7種原始值類型)
3)prototype/__proto__原型屬性值也是對象(排除Function.prototype)
3、大部分函數(重點是構造函數) 都內置一個prototype(原型「顯式原型」)的屬性,屬性值是一個對象,對象中存儲的屬性和方法,是供當前類所屬實例,調用的“公共”的屬性和方法
1)箭頭函數是沒有prototype屬性的;
2)在原型對象上有一個內置的屬性 constructor(構造器),屬性值是當前函數本身;
4、每一個對象都內置一個__proto__(原型鏈「隱式原型」)的屬性,屬性值指向自己所屬類的原型prototype對象。
1)Object.prototype這個對象的__proto__值是null,因為Object是所有對象的“基類”
只繪制“堆內存”,畫圖分析:( 有圖有真相 )
解析說明:
每一個數組都是Array類的實例,所以每一個數組的_proto_一定指向Array.prototype;
每一個對象都是Object類的實例,所以Array.prototype對象中的_proto_屬性指向Object.prototype;
原型鏈的查找機制
arr[1]或者 arr.push() 再或者 arr.hasOwnProperty()…
1)首先查找當前實例對象的私有屬性,私有中有,獲取就是私有的;
2)如果私有中沒有,則瀏覽器默認基于_proto_找其所屬類原型(prototype)上的公共屬性和方法;
3)如果還找不到,則基于原型對象上的_proto_繼續向上查找 … 直到找到Object.prototype為止。最終到null。
例如:arr.push <=> arr._proto_.push <=> Array.prototype.push
1)找到的方法都是相同的;
2)區別是方法執行時候,里面的this 不同;
(1)arr.push() arr首先基于原型鏈查找機制,找到Array.prototype上的push 方法,并且把方法執行,方法中的this -> arr;
(2)arr._proto_.push() 直接跳過私有屬性的查找,找公共的,方法執行的時候,方法中的this -> arr._proto_;
Array.prototype.push() this -> Array.prototype
3)_proto_ 在IE瀏覽器中進行訪問。(那如果代碼需要用到_proto_ ,怎么在IE中用呢? 結果:不能用。)
arr.hasOwnProperty('push') -> false
Array.prototype.hasOwnProperty('push') -> true
公有還是私有屬性,它是有參照物的
1)存儲在自己的堆內存中的屬性是 “私有的”;
2)基于_proto_查找到的屬性和方法有“公有的”;
每一個數組“即是數組也是對象”,因為它們可以調用Array.prototype和Object.prototype的屬性和方法。
* 構造函數、原型與實例之間的關系
關系解析說明:
每個構造函數都有一個prototype屬性指向它的原型對象,原型對象的constructor 指向構造函數,通過new 構造函數 生成實例,實例的__proto__屬性指向原型對象。
? 原型與原型鏈
function Fn() { this.x = 100; this.y = 200; this.getX = function () { console.log(this.x); } } Fn.prototype.getX = function () { console.log(this.x); }; Fn.prototype.getY = function () { console.log(this.y); }; let f1 = new Fn; let f2 = new Fn; console.log(f1.getX === f2.getX); // false 「都是私有的方法」 console.log(f1.getY === f2.getY); // true 「都是公共的方法」 console.log(f1.__proto__.getY === Fn.prototype.getY); // true console.log(f1.__proto__.getX === f2.getX); // false console.log(f1.getX === Fn.prototype.getX); // false console.log(f1.constructor); // fn console.log(Fn.prototype.__proto__.constructor); // Object f1.getX(); f1.__proto__.getX(); f2.getY(); Fn.prototype.getY();
畫圖分析:( 有圖有真相 )
解析說明:
1)先確定執行哪個方法「私有|公有」;
2)再確定執行方法中的this;
3)最后方法執行,計算機需要的結果即可;
f1.getX()
執行的私有方法,this -> f1
console.log(f1.x) => 100
f1._proto_.getX()
執行的公有方法,this -> f1._proto_
console.log(f1._proto_.x) => undefined
f2.getY()
執行的公有方法,this -> f2
console.log(f2.y) => 200
Fn.prototype.getY()
執行的公有方法,this -> Fn.prototype
console.log(Fn.prototype.y) => undefined
重寫內置new以及基于內置類原型擴展方法
* 核心答案 | 基礎知識要夯實
? * 1、new執行的原理 - 面試題( 面試常問 )
function Dog(name) { this.name = name; } Dog.prototype.bark = function () { console.log('wangwang'); } Dog.prototype.sayName = function () { console.log('my name is ' + this.name); } /* let sanmao = new Dog('三毛'); sanmao.sayName(); sanmao.bark(); */ function _new() { //=>完成你的代碼 } let sanmao = _new(Dog, '三毛'); sanmao.bark(); //=>"wangwang" sanmao.sayName(); //=>"my name is 三毛" console.log(sanmao instanceof Dog); //=>true
解決方法一( __proto__ 在IE瀏覽器兼容很差,不建議使用 )
function _new(Ctor, ...params) { // Ctor->Dog params->['三毛'] let obj = {}; obj.__proto__ = Ctor.prototype; // this->指向創建的實例對象 基于call方法改變即可 let result = Ctor.call(obj, ...params); if (/^(object|function)$/.test(typeof result)) return result; return obj; }
解析說明:
1、創建一個實例對象 實例對象.__proto__===所屬類.prototype;
2、會把構造函數當做普通函數執行「私有上下文、作用域鏈、初始THIS、形參賦值...」;
3、觀察函數執行的返回值,如果沒有返回值或者返回的是基本數據類型值,默認返回的都是實例對象,否則以自己返回的值為主。
Object.create([pro]):創建一個空對象,把[pro]作為當前創建空對象的__proto__的指向(把[pro]作為當前創建空對象的原型)。
1、[pro]可以傳遞null或者一個對象;
2、如果傳遞的是null,則當前空對象不具備__proto__的屬性,也就是不屬于任何類的實例。
? 例如:
let pro = { A: 10, B: 20 }; //Uncaught TypeError: Object prototype may only be an Object or null: undefined console.log(Object.create()); console.log(Object.create(null));
解決方法二( 在IE6,7,8瀏覽器不兼容 )
function _new(Ctor, ...params) { let obj = Object.create(Ctor.prototype); let result = Ctor.call(obj, ...params); if (/^(object|function)$/.test(typeof result)) return result; return obj; }
解決方法三 ( 兼容性比較好 )
// 重寫的方法只考慮pro傳遞的是一個對象 Object.create = function (pro) { function Proxy() {} Proxy.prototype = pro; return new Proxy; }; function _new(Ctor) { // 獲取除第一個實參以外,剩余傳遞的參數信息,以數組的形式保存到params中 var params = [].slice.call(arguments, 1); // Object.create兼容IE低版本瀏覽器,需要改寫 var obj = Object.create(Ctor.prototype); // 基于apply既可以改變this,也可以把數組中的每一項傳遞給函數 var result = Ctor.apply(obj, params); if (/^(object|function)$/.test(typeof result)) return result; return obj; }
* 2、擴展內置類原型上的方法
1、調用的時候更加方便;
2、也更好的實現鏈式調用;
* 注意:自己編寫的方法會覆蓋內置的方法,所以自己命名的時候需要注意,一般都是設置前綴,例如:myUnique。
? 常用數組去重方法
function unique(arr) { // 首先基于Set結構去重,最后轉換為數組 let result = new Set(arr); result = Array.from(result); return result; } let arr = [1, 2, 3, 2, 3, 4, 2, 3, 4, 2, 1, 2, 3, 4, 5, 3, 4]; let result = unique(arr); console.log(result);
? 先去重,再排序 - 面試題( 面試常問 )
Array.prototype.unique = function unique() { // this->arr 一般是當前操作類的實例 let result = new Set(this); result = Array.from(result); return result; //返回的結果還是一個數組,則可以繼續調用數組的其它方法 ->“鏈式調用” };
// 先去重,再排序 // + sort是Array.prototype上的方法,所以數組可以直接調用 let arr = [1, 2, 3, 2, 3, 4, 2, 3, 4, 2, 1, 2, 3, 4, 5, 3, 4]; let result = arr.unique().sort((a, b) => a - b); console.log(arr, result);
THIS情況匯總及CALL、APPLY、BIND的應用
* 核心答案 | 基礎知識要夯實
1、THIS的幾種情況
1)事件綁定;
2)函數執行:1)自執行函數 2)回調函數;
3)構造函數執行;
4)基于call /apply /bind 改變函數中的this;
5)箭頭函數中沒有自己的this,所用到的this是使用其上下文中的;
說明:Function.prototype -> call/apply/bind 所有的函數都可以調取這三個辦法。
Function.prototype.call = function call(context) { // this->fn // context->obj // ... };
? 基于call /apply /bind 改變函數中的this
window.name = 'WINDOW'; let obj = { name: '前端學苑', age: 2 }; function fn(x, y) { console.log(this, x + y); } fn(); //this->window obj.fn(); //Uncaught TypeError: obj.fn is not a function
fn.call(obj); //this->obj fn.call(obj, 10, 20); //this->obj x->10 y->20 fn.call(); //this->window 嚴格模式下undefined fn.call(null); //this->window 嚴格模式下null 「傳遞的是undefiend也是如此」 fn.call(10, 20); //this->10「對象」 x->20 y->undefined
解析說明:
fn.call(obj);
底層處理方式:fn先基于__proto__找到Function.prototype.call,把call方法執行的時候,call方法內部實現了一些功能:會把fn執行,并且讓fn中的this變為第一個實參值。
* apply的作用和細節上和call一樣,只有一個區別:傳遞給函數實參的方式不一樣。
fn.call(obj, 10, 20); fn.apply(obj, [10, 20]);
最后結果和call是一樣的,只不過apply方法執行的時候要求:傳遞給函數的實參信息都要放置在一個數組中,但是apply內部也會向call方法一樣,把這些實參信息一項項的傳遞給函數。
? 需求:獲取數組中的最大值
let arr = [10, 30, 15, 36, 23];
* 方法一:先排序
arr.sort(function (a, b) { return b - a; }); let max = arr[0]; console.log('數組中的最大值是:' + max);
* 方法二:假設法
第一種方法:
let max = arr[0]; for (let i = 1; i < arr.length; i++) { let item = arr[i]; if (item > max) { max = item; } } console.log('數組中的最大值是:' + max);
第二種方法:
let max = arr.reduce((result, item) => { return item > result ? item : result; }); console.log('數組中的最大值是:' + max);
* 方法三:借用Math.max
第一種方法:
Math.max(10, 30, 15, 36, 23) ->36 獲取一堆數中的最大值 Math.max([10, 30, 15, 36, 23]) ->NaN 傳遞一個數組是不行的
第二種方法:ES6展開運算符
let max = Math.max(...arr); console.log('數組中的最大值是:' + max);
第三種方法:基于apply的特點
let max = Math.max.apply(null, arr); console.log('數組中的最大值是:' + max);
第四種方法:字符串拼接成為最終想要的表達式
let str = `Math.max(${arr})`; let max = eval(str); console.log('數組中的最大值是:' + max);
? 需求:把類數組集合轉換為數組集合
重寫內置的slice,實現淺克隆;
Array.prototype.slice = function slice() { // 重寫內置的slice,實現淺克隆 // this->ary let arr = []; for (let i = 0; i < this.length; i++) { let item = this[i]; arr.push(item); } return arr; }; let ary = [10, 20, 30]; let newAry = ary.slice(); //不傳遞或者傳遞0 -> 數組的淺克隆 console.log(newAry, newAry === ary);
畫圖分析:( 有圖有真相 )
區別在于:
1、內置代碼中用的是this,自己寫的代碼中用的是argument。
2、如果我們可以 讓內置的slice執行,并且把方法中的 this改變為arguments -> 這樣其實就是把類數組集合克隆(轉換)為數組。
3、讓內置slice執行:找到slice,加小括號一執行即可。
1)Array.prototype.slice() 2)[].slice()
改變方法中的this
1)call 2)apply
4、[].slice.call(arguments) 把類數組轉換為數組
重要:數組中大部分方法,都可以基于這樣的原理(改變this),實現類數組的借用。原因:類數組除了不是Array的實例,和數組的結果是一致的,所以操作數組的一些代碼(類似于循環等操作)也一定適用于類數組,所以可以實現方法的借用。
求和第一種方法:
function sum() { // arguments:實參集合,它是一個類數組,不是Array的實例,所以不能直接調用Array.prototype上的方法,但是結構和數組非常的相似,都是索引+length // 第一種方法 let arr = []; for (let i = 0; i < arguments.length; i++) { let item = arguments[i]; arr.push(item); } // 第二種方法 // let arr = [].slice.call(arguments); return arr.reduce((result, item) => item + result); } let total = sum(10, 20, 30, 40);
求和第二種方法,ES6實現:
function sum(...arr) { // 第一種方法:... arr 基于剩余運算符獲取的實參集合本身就是一個數組 // 第二種方法:Array.from:可以把一個類數組(或者Set)轉換為數組 // let arr = Array.from(arguments); // 第三種方法:基于展開運算符把類數組中的每一項拿出來,分別賦值給數組 // let arr = [...arguments]; return arr.reduce((result, item) => item + result); } let total = sum(10, 20, 30, 40); console.log(total); // 結果:100
? this指向的例子
let obj = { name: '前端學苑', age: 11 }; function fn(x, y) { console.log(this, x, y); }
1、操作一:
document.body.onclick = fn;
分析:
1)事件綁定的時候方法是沒有執行的,只有事件觸發,瀏覽器會幫助我們把方法執行;
2)this->body;
3)x->MouseEvent 事件對象「瀏覽器不僅幫助我們把方法執行,而且還把存儲當前操作的信息的事件對象傳遞給函數」;
4)y->undefined;
2、操作二:
setTimeout(fn, 1000);
分析:
1)設置一個定時器(此時綁定的函數沒有執行,此時只是綁定一個方法),1000MS后,瀏覽器會幫助我們把fn執行;
2)this->window;
3)x->undefined;
4)y->undefined;
我們期望:不論是事件觸發,還是定時器到時間,執行對應的方法時,可以改變方法中的this,以及給方法傳遞實參信息。
1、“立即處理的思想”
直接下屬這種操作辦法是不可以的:call/apply在處理的時候,會把函數立即執行,也就是在事件綁定或者設置定時器的時候,fn就執行了,而不是等待事件觸發或者定時器到時間后再執行 “立即處理的思想”。
代碼如下:
document.body.onclick = fn.call(obj, 10, 20); setTimeout(fn.call(obj, 10, 20), 1000);
2、“預先處理思想「柯理化函數」”
我們綁定方法的時候(不論是事件綁定還是設置定時器),先綁定一個匿名函數,事件觸發或者達到時間,先把匿名函數執行,在執行匿名函數的時候,再把我們需要執行的fn執行,此時就可以基于call/apply改變this和參數信息了。
代碼如下:
document.body.onclick = function (ev) { //this->body fn.call(obj, 10, 20, ev); }; setTimeout(function () { //this->window fn.call(obj, 10, 20); }, 1000);
bind相當于call/apply來講,并不會把函數立即執行,只是實現處理了要改變的this和參數,一切的執行還是按照原有的時間或者觸發節點進行。
代碼如下:
document.body.onclick = fn.bind(obj, 10, 20); setTimeout(fn.bind(obj, 10, 20), 1000);
* 箭頭函數沒有自己的this
? 實例一:
let obj = { name: '前端學苑', age: 11, fn: function () { // this->obj let that = this; return function () { // this->window // 如果需要改變obj.name,可以用that替換this that.name = 'FE2020'; console.log(this); }; } }; let f = obj.fn(); f();
? 實例二:
let obj = { name: '前端學苑', age: 11, fn: function () { // this->obj return () => { this.name = 'FE2020'; console.log(this); // {name:'FE2020', age: 11, fn:f} }; } }; let f = obj.fn(); f.call(100);
說明:
箭頭函數沒有this(方法執行的時候不存在初始this這一項操作),所以基于call/apply操作它都是無用的,沒有this。
看完上述內容,你們對Javascript中面向對象和原型原型鏈是怎樣的有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。