javascript高級部分
Function方法 與 函數式編程
call
語法:call([thisObj[,arg1[, arg2[, [,.argN]]]]])
定義:調用一個對象的一個方法,以另一個對象替換當前對象。
說明:call 方法可以用來代替另一個對象調用一個方法。call 方法可將一個函數的對象上下文從初始的上下文改變為由 thisObj 指定的新對象。
如果沒有提供 thisObj 參數,那么 Global 對象被用作 thisObj。
let myName = 'goudan';let myAge = 13; function showMsg(msg){return (msg + '').toLowerCase();}showName(myName); // 'goudan'
這段代碼很容易就能看懂,在實際開發工作中,我們會處理不同的數據集合,這時候聲明單一變量已經無法滿足胃口,需要通過json的形式來存儲數據
let person = {name:"kyogre",age:13,hobby:"coding"}let newPerson ={name:'dachui',age:99,hobby:'eat'}function showMsg(msg){return (msg + '').toLowerCase();}showMsg(person.name) // 'kyogre'showMsg(newPerson.name) // 'dachui'
存儲數據的方式發生了改變,但是我們處理數據的方式還是那么。。。古老,如果業務復雜一點
function format(msg){return msg+''.toLowerCase();}function show(msg){return '信息的內容是:'+ format(msg);}show(person.name) // '信息內容是:kyogre'show(newPerson.name) // '信息內容是:dachui'
顯示的傳遞上下文對象 (穿參)使得業務越來越復雜,這種疊加會讓開發變得冗余和難以解讀,bug和閉包橫飛
那我們看看通過this如何清晰解決問題
通常this不會指向函數自身,而是調用函數的對象主體。當然,我們可以強制讓function自身成為對象主體,這個以后咱們討論; json本身就是對象,我們是否可以這樣:
const person = {name:"kyogre",age:13,hobby:"coding"}const newPerson ={name:'dachui',age:99,hobby:'eat'}function format(){return this.name+''.toLowerCase();}
問題來了,不讓穿參這個format中的this指向誰呢? 指向調用format的對象本身,可是調用主體并不明確,可能是person也可能是newPerson,這時回過頭看看call的定義吧:調用一個對象的一個方法,以另一個對象替換當前對象。 將函數內部執行上下文對象由原始對象替換為指定對象
const name = '我叫window'; //全局變量 非嚴格模式下都為 window的屬性 window.name;function format(){return (this.name + '').toLowerCase();}format(); //'我叫window';
不要驚慌,本身他就是這樣window會作為調用頂級作用域鏈函數的對象主體;這里的this默認為 window, 用person對象代替window這個默認this主體去執行format會怎么樣呢
format.call(person); // kyogre format.call(newPerson);// dachuifunction show(){return '信息的內容是:'+ format.call(this);}show.call(person); // 信息的內容是:kyogre
感覺自己了解了this和call的小明,已經肆無忌憚的笑了起來,這樣他就可以從繁重的回調與參數傳遞中解脫了,并且能夠實現方法的初級模塊化。
下面可以用call做一些平常的操作
function isArray(object){return Object.prototype.toString.call(object) == '[object Array]';}// 借用Object原型上的toString方法來驗證下對象是否是數組?function accumulation(){return [].reduce.call(arguments,(a,b)=>{return a+b}}//讓不能使用數組方法的arguments類數組集合使用借用數組的reduce方法return Array.prototype.forEach.call($$('*'),(item)=>{item.style.border = '1px solid red';}//把類數組變成數組對象function fn() {let args = [].slice.call(arguments);console.log(args)}fn(1,2,3,4,5)//[1,2,3,4,5]
使用call可以使類數組具有數組的方法
類數組和數組結構類似,有length方法,但是不能調用數組的方法,借助call可以使用數組的方法
-
獲取每一個li的innerText + 10的結果存放到一個數組中
<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>call語法</title> </head> <body><ul><li>1</li><li>2</li><li>3</li><li>4</li></ul><script>const aLi = document.querySelectorAll('ul>li'); //類數組 元素集合 const liArr = [].map.call(aLi, function (item) {return Number(item.innerText) + 10;})console.log(liArr);</script> </body> </html>
-
計算所有參數的和
// 以前不用call只能循環疊加arguments的值 function add() {var count = arguments[0];for (let i = 1, len = arguments.length; i < len; i++) {count += arguments[i];}return count;}//arguments也是類數組,可以借助數組reduce迭代求和 function add() {return [].reduce.call(arguments, function (acc, curr) {return acc + curr;}) } console.log(add(1, 2, 3, 4, 5.5, 6));
-
商品轉化成字符串形式 格式化
<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>this 與 call</title> </head> <body><ul id="product"></ul><script>const oUl = document.querySelector('#product');/* 商品轉化成字符串形式 格式化商品名稱:吸塵器 , 商品價格:$199, 商品庫存:999*/const PRODUCT_DATA = [{UID: '018945389743211',productName: '吸塵器',productPrice: '$199',productCount: 999},{UID: '018945389743213',productName: '機器人',productPrice: '$1199',productCount: 222},{UID: '018945389742234',productName: '鼠標',productPrice: '$19',productCount: 111}];function format(idx) {return `商品序號:${idx} 商品名稱:${this.productName} , 商品價格:<span>${this.productPrice}</span> , 商品庫存:${this.productCount} `}function createTemplate() {let str = ''this.forEach(function (item, idx) {str += `<li>${format.call(item, idx)}</li>`;});return str;}function drawList(str) {oUl.innerHTML = str;}drawList(createTemplate.call(PRODUCT_DATA));// 跳過渲染的行為,直接去改變頁面上對元素進行增刪改查,是不被允許的// 使用call可以對DOM元素使用數組的方法,但只要涉及實際DOM渲染,是不被允許的sortList();//對商品價格進行排序,但實際排序并沒有改變function sortList() {var tempArr = [].slice.call(oUl.children);console.log(oUl.children);tempArr.sort(function (a, b) {const aPrice = parseFloat(a.querySelector('span').innerText);const bPrice = parseFloat(b.querySelector('span').innerText);return aPrice - bPrice;});console.log(tempArr);}</script> </body> </html>
apply
語法:func.apply(thisArg, [argsArray])
call()方法的作用和 apply() 方法類似,區別就是
call()
方法接受的是參數列表,而apply()
方法接受的是一個參數數組。方法.call(替換對象,參數1,參數2,…參數n);
方法.apply(替換對象,[參數1,參數2,參數3…,參數n]);
apply()經常和arguments成對去使用
const person = {fullName: function(city, country) {return this.firstName + " " + this.lastName + "," + city + "," + country;}
}
const person1 = {firstName:"John",lastName: "Doe"
}
person.fullName.apply(person1, ["Oslo", "Norway"]);
//apply()經常和arguments成對去使用
//返回數組中的最大項目
function getMaxNum() {console.log(arguments);//類數組 [參數1,參數2,.....,參數n]return Math.max.apply(null, arguments);
}
console.log(getMaxNum(1, 2, 3, 45, 6, 78, 23, 12, 4, 644, 12343));//12343
Math.max(1, 2, 3, 45, 6, 78, 23, 12, 4, 644, 12343)//12343
//如果是數組參數,可以用apply,當然更好的方法是用解構
Math.max.apply(null, [1,2,3]); // 3
Math.max(...[1,2,3]); // 3//沒有concat拼接數組方法之前,可以用push完成concat方法,了解即可
const arr = [1,2,3];
const otherArr = [3,4,5];
arr.push.apply(arr,otherArr);
console.log(arr); // [1, 2, 3, 3, 4, 5]
arr.push(otherArr);
console.log(arr); // [1, 2, 3, [3,4,5]]
案例:自定義任意數量的li內容添加到ul中
<!DOCTYPE html>
<html lang="zh-cn">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>apply</title>
</head>
<body><ul class="box">你好,我是box</ul><script>const oBox = document.querySelector('.box');const ARRAY = ['你好', '我好', '大家好']function appendList(ele) {var args = [].slice.call(arguments, 1);ele.innerHTML = format.apply(null, args);}appendList(oBox, '你好', '我好', '大家好');function format() {return [].reduce.call(arguments, function (acc, curr) {acc += `<li>${curr}</li>`;return acc;}, '');}</script>
</body>
</html>
案例:參數一是函數,后面的參數作為參數一函數的參數
/*fn1參數 cb,...不定參返回值 cb的運行結果(返回值)cb fn1的實參 函數 回調函數參數形參: 不定實參: fn1的參數列表(not:cb) fn1的除了第一位的剩余實參返回值:reduce 歸并結果reduce 調用主體對象 call (cb的實參列表對象)返回值: cb的實參列表對象的累加和reduce的返回結果是cb的實參列表累加和cb的實參列表是 fn1的 實參列表.slice(1)*/
function fn1(cb) {let args = [].slice.call(arguments, 1);return cb.apply(null, args);}let result = fn1(function () {return [].reduce.call(arguments, function (acc, curr) {return acc + curr;});}, 2, 3, 4, 5, 6); //20console.log(result);
柯理化函數(currying)
在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。
柯里化函數的原理是利用閉包機制來實現的
//將以下寫法轉換成柯里化寫法
function fnx(x, y, z) {return x + y + z;
}
//柯里化寫法
function fn(x) {return function fn1(y) {x = x + y;return function fn2(z) {return x + z;}}
}
console.log(fn(3)(4)(5));
柯里化函數是利用閉包來實現的
閉包的機制:如果變量還被需要,就不會被回收為持有狀態
注意:實際上大多數情況要避免閉包的使用,沒什么好處,必須需要局部變量持久化的情況下才用,因為性能開銷很大,因為本身垃圾回收機制是瀏覽器的自我優化,本身就是用來清理垃圾的
//閉包的原理:fn執行會返回fn2,fn2還沒有入棧,永遠是待執行狀態,所以w永遠是持有狀態,就不會被回收
function fn() {let w = 20;return function fn2() { w++;console.log(w);}
}
fn()()//21 第二次調用已經執行完畢,w被回收
fn()()//21
fn()()//21var x = fn()
x()//21 函數fn2被掛載在x上,永遠不會被回收
x()//22
x()//23var x = fn()
var y = fn()
x()//21 x和y是互不干擾互不相通的
y()//21
柯里化函數應用性封裝
以上寫法只能柯3下,而且一次只能限定傳一個參數,無限往下柯里化,并且每次都任何數量的參數寫法如下
function currying(fn) {const args = Array.prototype.slice.call(arguments, 1);const inlay = function () {if (arguments.length === 0) {return fn.apply(this, args);}if(arguments.length > 0 ){Array.prototype.push.apply(args, arguments);return inlay;} }return inlay;}function add() {const vals = Array.prototype.slice.call(arguments);return vals.reduce((pre, val) => {return pre + val;});}let newAdd = currying(add, 1, 2, 3);newAdd(4, 5);newAdd(6, 7)(6)(2, 3);console.log(newAdd()); //39
bind
bind()
方法創建一個新的函數,在bind()
被調用時,這個新函數的this
被指定為bind()
的第一個參數,而其余參數將作為新函數的默認參數,供調用時使用。和call apply的區別在于不是主動執行函數的時候進行this的偏轉 ,而是在函數聲明或者賦值的時候綁定bind
語法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
參數:
- thisArg
調用綁定函數時作為
this參數傳遞給目標函數的值。 如果使用[
new](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)運算符構造綁定函數,則忽略該值。當使用
bind在
setTimeout中創建一個函數(作為回調提供)時,作為
thisArg傳遞的任何原始值都將轉換為
object。如果
bind函數的參數列表為空,或者
thisArg是
null或
undefined,執行作用域的
this將被視為新函數的
thisArg。
- arg1, arg2, …
當目標函數被調用時,被預置入綁定函數的參數列表中的參數。
返回值:
返回一個原函數的拷貝,并擁有指定的 **
this** 值和初始參數。
const OBJ = {petName: 'kyogre',qq: '2971411050',sayHi: function () {console.log(`我是${this.petName} 很高興認識你`)}
}let sayHi = OBJ.sayHi;
sayHi(); //我是undifined 很高興認識你 ps: this非嚴格模式指向了window window.petName不存在let sayHi = OBJ.sayHi.bind(OBJ);
sayHi(); //我是kyogre 很高興認識你 ps:通過bind強制綁定sayHi函數內部的this指向OBJ
用bind給函數指定對象主體,即使用call也無法強行改變
const MIMI = {age: 1,add: function () {this.age++;console.log(this.age);}}const OUA = {age: 4,add: function () {console.log(this.age);}}let add = MIMI.add.bind(MIMI);add.call(OUA); //2 add已經被永久性綁定給MIMI對象主體了,內部this]永遠指向MIMI,即使用call也無法改變
當使用定時器的時候,無法用call或apply,因為call是主動執行函數返回函數值,在定時器中第一個參數要放的是函數體,而不是函數值,所以為了使用定時器時,為了保證內部this,只能使用bind
const MIMI = {age: 1,add: function () {this.age++;console.log(this.age);}}function fn(cb) {// cb && cb.call(MIMI);setInterval(cb.bind(MIMI), 100)//cb.bind(MIMI)返回的時一個新函數,并且新函數永遠指向MINI}fn(function () {console.log(this.age);//1})
偏函數的應用:當需要使用某些函數,并且該函數有默認參數的時候,可以使用bind給其設置默認參數
function fn() {return [].reduce.call(arguments, function (acc, curr) {return acc + curr;});}let partial = fn.bind(null, 1,2,3,4);console.log(partial(20));//30
偏函數 (partial)
在計算機科學中,局部應用(偏函數)是指固定一個函數的一些參數,然后產生另一個更小元的函數。(什么是元?元是指函數參數的個數,比如一個帶有兩個參數的函數被稱為二元函數。)
bind()
的另一個最簡單的用法是使一個函數擁有預設的初始參數。只要將這些參數(如果有的話)作為bind()
的參數寫在this
后面。當綁定函數被調用時,這些參數會被插入到目標函數的參數列表的開始位置,傳遞給綁定函數的參數會跟在它們后面。一般用于做科學開發,一般我們做小的工具開發的時候,有一些固定的初始值,這些初始值時不變的,函數的初始值雖然可以寫到函數內部,但是行為,數據和邏輯一般要分離,就可以用到偏函數,可以擴展函數的多樣性
function list() {return Array.prototype.slice.call(arguments);
}function addArguments(arg1, arg2) {return arg1 + arg2
}const list1 = list(1, 2, 3); // [1, 2, 3]const result1 = addArguments(1, 2); // 3// 創建一個函數,它擁有預設參數列表。
const leadingThirtysevenList = list.bind(null, 37);// 創建一個函數,它擁有預設的第一個參數
const addThirtySeven = addArguments.bind(null, 37); const list2 = leadingThirtysevenList();
// [37]const list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]const result2 = addThirtySeven(5);
// 37 + 5 = 42 const result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二個參數被忽略
通道函數(compose)
按照順序
const double = x => x + x;const triple = x => 3 * x;const quarter = x => x / 4;//調用pipe 根據參數順序 組裝返回一個新的函數 function pipe(...funcs) {return function (input) {return funcs.reduce(function (acc, curr) {return curr(acc);}, input);}}//通道函數用箭頭函數簡化const pipe = (...funcs) => input => funcs.reduce((acc, curr) => curr(acc), input);var result = pipe(quarter, double);console.log(result(10));//5
function toUpperCase(str){return str.toUpperCase(); // 將字符串變成大寫}function add(str){return str + '!!!'; // 將字符串拼接}function split(str){return str.split(''); // 將字符串拆分為數組}function reverse(arr){return arr.reverse(); // 將數組逆序}function join(arr){return arr.join('-'); // 將數組按'-'拼接成字符串}function compose(){const args = Array.prototype.slice.call(arguments); // 轉換為數組使用下面的方法return function(x){return args.reduceRight(function(result, cb){return cb(result);}, x);}}const f = compose(add, join, reverse, split, toUpperCase);console.log( f('cba') ); // A-B-C!!!
柯理化與偏函數區別
-
柯里化是將一個多參數函數轉換成多個單參數函數,也就是將一個 n 元函數轉換成 n 個一元函數。
-
局部應用則是固定一個函數的一個或者多個參數,也就是將一個 n 元函數轉換成一個 n - x 元函數。
面向對象編程
什么是對象
Everything is object (萬物皆對象)
對象到底是什么,我們可以從兩次層次來理解。
(1) 對象是單個事物的抽象。
一本書、一輛汽車、一個人都可以是對象,一個數據庫、一張網頁、一個與遠程服務器的連接也可以是對象。當實物被抽象成對象,實物之間的關系就變成了對象之間的關系,從而就可以模擬現實情況,針對對象進行編程。
(2) 對象是一個容器,封裝了屬性(property)和方法(method)。
屬性是對象的狀態,方法是對象的行為(完成某種任務)。比如,我們可以把動物抽象為animal對象,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。
在實際開發中,對象是一個抽象的概念,可以將其簡單理解為:數據集或功能集。
ECMAScript-262 把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。
嚴格來講,這就相當于說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都
映射到一個值。
提示:每個對象都是基于一個引用類型創建的,這些類型可以是系統內置的原生類型,也可以是開發人員自定義的類型。
什么是面向對象
面向對象不是新的東西,它只是過程式代碼的一種高度封裝,目的在于提高代碼的開發效率和可維護性。
面向對象編程 —— Object Oriented Programming,簡稱 OOP ,是一種編程開發思想。
它將真實世界各種復雜的關系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。
在面向對象程序開發思想中,每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。
因此,面向對象編程具有靈活、代碼可復用、高度模塊化等特點,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合作的大型軟件項目。
面向對象與面向過程:
- 面向過程就是親力親為,事無巨細,面面俱到,步步緊跟,有條不紊
- 面向對象就是找一個對象,指揮得結果
- 面向對象將執行者轉變成指揮者
- 面向對象不是面向過程的替代,而是面向過程的封裝
面向對象的特性:
- 封裝性
封裝,也就是把客觀事物封裝成抽象的類,并且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
- 繼承性
繼承是指這樣一種能力:它可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展。
- 多態性
基于對象所屬類的不同,外部對同一個方法的調用,實際執行的邏輯不同。
程序中面向對象的基本體現
在 JavaScript 中,所有數據類型都可以視為對象,當然也可以自定義對象。
自定義的對象數據類型就是面向對象中的類( Class )的概念。
我們以一個例子來說明面向過程和面向對象在程序流程上的不同之處。
假設我們要處理學生的成績表,為了表示一個學生的成績,面向過程的程序可以用一個對象表示:
let student1 = { name: 'Michael', score: 98 }
let student2 = { name: 'Bob', score: 81 }
而處理學生成績可以通過函數實現,比如打印學生的成績:
function printScore (student) {console.log(`姓名:${student.name} 成績:${student.score}`);
}
如果采用面向對象的程序設計思想,我們首選思考的不是程序的執行流程,
而是 Student
這種數據類型應該被視為一個對象,這個對象擁有 name
和 score
這兩個屬性(Property)。
如果要打印一個學生的成績,首先必須創建出這個學生對應的對象,然后,給對象發一個 printScore
消息,讓對象自己把自己的數據打印出來。
抽象數據行為模板(Class):
function Student (name, score) {this.name = namethis.score = score
}Student.prototype.printScore = function () {console.log(`姓名:${this.name} 成績:${this.score}`);
}
根據模板創建具體實例對象(Instance):
var std1 = new Student('Michael', 98);
var std2 = new Student('Bob', 81);
實例對象具有自己的具體行為(給對象發消息):
std1.printScore() // => 姓名:Michael 成績:98
std2.printScore() // => 姓名:Bob 成績 81
面向對象的設計思想是從自然界中來的,因為在自然界中,類(Class)和實例(Instance)的概念是很自然的。
Class 是一種抽象概念,比如我們定義的 Class——Student ,是指學生這個概念,
而實例(Instance)則是一個個具體的 Student ,比如, Michael 和 Bob 是兩個具體的 Student 。
所以,面向對象的設計思想是:
- 抽象出 Class
- 根據 Class 創建 Instance
- 指揮 Instance 得結果
面向對象的抽象程度又比函數要高,因為一個 Class 既包含數據,又包含操作數據的方法。
創建對象
簡單方式new Object()
我們可以直接通過 new Object()
創建:
const person = new Object()
person.name = 'Jack'
person.age = 18person.sayName = function () {console.log(this.name)
}
每次創建通過 new Object()
比較麻煩,所以可以通過它的簡寫形式對象字面量來創建:
const person = {name: 'Jack',age: 18,sayName: function () {console.log(this.name)}
}
對于上面的寫法固然沒有問題,但是假如我們要生成兩個 person
實例對象呢?
const person1 = {name: 'Jack',age: 18,sayName: function () {console.log(this.name)}
}const person2 = {name: 'Mike',age: 16,sayName: function () {console.log(this.name)}
}
通過上面的代碼我們不難看出,這樣寫的代碼太過冗余,重復性太高。
簡單方式的改進:工廠函數
我們可以寫一個函數,解決代碼重復問題:
function createPerson (name, age) {return {name: name,age: age,sayName: function () {console.log(this.name)}}
}
然后生成實例對象:
let p1 = createPerson('Jack', 18)
let p2 = createPerson('Mike', 18)
這樣封裝確實爽多了,通過工廠模式我們解決了創建多個相似對象代碼冗余的問題,
但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
構造函數
一種更優雅的工廠函數就是下面這樣,構造函數:
function Person (name, age) {this.name = namethis.age = agethis.sayName = function () {console.log(this.name)}
}let p1 = new Person('Jack', 18)
p1.sayName() // => Jacklet p2 = new Person('Mike', 23)
p2.sayName() // => Mike
解析構造函數代碼的執行
在上面的示例中,Person()
函數取代了 createPerson()
函數,但是實現效果是一樣的。
這是為什么呢?
我們注意到,Person()
中的代碼與 createPerson()
有以下幾點不同之處:
- 沒有顯示的創建對象
- 直接將屬性和方法賦給了
this
對象 - 沒有
return
語句 - 函數名使用的是大寫的
Person
而要創建 Person
實例,則必須使用 new
操作符。
以這種方式調用構造函數會經歷以下 4 個步驟:
- 創建一個新對象
- 將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象)
- 執行構造函數中的代碼
- 返回新對象
下面是具體的偽代碼:
function Person (name, age) {// 當使用 new 操作符調用 Person() 的時候,實際上這里會先創建一個對象// var instance = {}// 然后讓內部的 this 指向 instance 對象// this = instance// 接下來所有針對 this 的操作實際上操作的就是 instancethis.name = namethis.age = agethis.sayName = function () {console.log(this.name)}// 在函數的結尾處會將 this 返回,也就是 instance// return this
}
構造函數和實例對象的關系
使用構造函數的好處不僅僅在于代碼的簡潔性,更重要的是我們可以識別對象的具體類型了。
在每一個實例對象中的__proto__中同時有一個 constructor
屬性,該屬性指向創建該實例的構造函數:
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
對象的 constructor
屬性最初是用來標識對象類型的,
但是,如果要檢測對象的類型,還是使用 instanceof
操作符更可靠一些:
console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true
總結:
- 構造函數是根據具體的事物抽象出來的抽象模板
- 實例對象是根據抽象的構造函數模板得到的具體實例對象
- 每一個實例對象都具有一個
constructor
屬性,指向創建該實例的構造函數- 注意:
constructor
是實例的屬性的說法不嚴謹,具體后面的原型會講到
- 注意:
- 可以通過實例的
constructor
屬性判斷實例和構造函數之間的關系- 注意:這種方式不嚴謹,推薦使用
instanceof
操作符,后面學原型會解釋為什么
- 注意:這種方式不嚴謹,推薦使用
構造函數的問題
使用構造函數帶來的最大的好處就是創建對象更方便了,但是其本身也存在一個浪費內存的問題:
function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = function () {console.log('hello ' + this.name)}
}let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)
在該示例中,從表面上好像沒什么問題,但是實際上這樣做,有一個很大的弊端。
那就是對于每一個實例對象,type
和 sayHello
都是一模一樣的內容,
每一次生成一個實例,都必須為重復的內容,多占用一些內存,如果實例對象很多,會造成極大的內存浪費。
console.log(p1.sayHello === p2.sayHello) // => false
對于這種問題我們可以把需要共享的函數定義到構造函數外部:
function sayHello = function () {console.log('hello ' + this.name)
}function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = sayHello
}let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)console.log(p1.sayHello === p2.sayHello) // => true
這樣確實可以了,但是如果有多個需要共享的函數的話就會造成全局命名空間沖突的問題。
你肯定想到了可以把多個函數放到一個對象中用來避免全局命名空間沖突的問題:
const fns = {sayHello: function () {console.log('hello ' + this.name)},sayAge: function () {console.log(this.age)}
}function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = fns.sayHellothis.sayAge = fns.sayAge
}let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true/*可以使用prototype原型來解決以上問題prototype 原型 NDA原型就是用來承載DNA中的行為的載體原型是一個對象1. 解決了行為方法作為屬性 在實例化的時候會被拷貝而導致內存緊張2. 解決了命名空間的沖突和方法與對象的從屬關系
*/
至此,我們利用自己的方式基本上解決了構造函數的內存浪費問題。
但是代碼看起來還是那么的格格不入,那有沒有更好的方式呢?
原型
內容引導:
- 使用 prototype 原型對象解決構造函數的問題
- 分析 構造函數、prototype 原型對象、實例對象 三者之間的關系
- 屬性成員搜索原則:原型鏈
- 實例對象讀寫原型對象中的成員
- 原型對象的簡寫形式
- 原生對象的原型
- Object
- Array
- String
- …
- 原型對象的問題
- 構造的函數和原型對象使用建議
更好的解決方案: prototype
Javascript 規定,每一個構造函數都有一個 prototype
屬性,指向另一個對象。
這個對象的所有屬性和方法,都會被構造函數的實例繼承。
這也就意味著,我們可以把所有對象實例需要共享的屬性和方法直接定義在 prototype
對象上。
function Person (name, age) {this.name = namethis.age = age
}console.log(Person.prototype)Person.prototype.type = 'human'Person.prototype.sayName = function () {console.log(this.name)
}let p1 = new Person(...)
let p2 = new Person(...)console.log(p1.sayName === p2.sayName) // => true
這時所有實例的 type
屬性和 sayName()
方法,
其實都是同一個內存地址,指向 prototype
對象,因此就提高了運行效率。
構造函數、實例、原型三者之間的關系
任何函數都具有一個 prototype
屬性,該屬性是一個對象。
function F () {}
console.log(F.prototype) // => objectF.prototype.sayHi = function () {console.log('hi!')
}
構造函數的 prototype
對象默認都有一個 constructor
屬性,指向 prototype
對象所在函數。
console.log(F.constructor === F) // => true
通過構造函數得到的實例對象內部會包含一個指向構造函數的 prototype
對象的指針 __proto__
。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
`__proto__` 是非標準屬性。
實例對象可以直接訪問原型對象成員。
instance.sayHi() // => hi!
總結:
- 任何函數都具有一個
prototype
屬性,該屬性是一個對象 - 構造函數的
prototype
對象默認都有一個constructor
屬性,指向prototype
對象所在函數
//函數,原型prototype和構造器constructor相當于以下關系,是偽代碼,實際更復雜const fn = {prototype: {constructor: fn}}
- 通過構造函數得到的實例對象內部會包含一個指向構造函數的
prototype
對象的指針__proto__
- 所有實例都直接或間接繼承了原型對象的成員
屬性成員的搜索原則:原型鏈
了解了 構造函數-實例-原型對象 三者之間的關系后,接下來我們來解釋一下為什么實例對象可以訪問原型對象中的成員。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性
- 搜索首先從對象實例本身開始
- 如果在實例中找到了具有給定名字的屬性,則返回該屬性的值
- 如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性
- 如果在原型對象中找到了這個屬性,則返回該屬性的值
也就是說,在我們調用 person1.sayName()
的時候,會先后執行兩次搜索:
- 首先,解析器會問:“實例 person1 有 sayName 屬性嗎?”答:“沒有。
- ”然后,它繼續搜索,再問:“ person1 的原型有 sayName 屬性嗎?”答:“有。
- ”于是,它就讀取那個保存在原型對象中的函數。
- 當我們調用 person2.sayName() 時,將會重現相同的搜索過程,得到相同的結果。
而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
總結:
- 先在自己身上找,找到即返回
- 自己身上找不到,則沿著原型鏈向上查找,找到即返回
- 如果一直到原型鏈的末端還沒有找到,則返回
undefined
實例對象讀寫原型對象成員
讀取:
- 先在自己身上找,找到即返回
- 自己身上找不到,則沿著原型鏈向上查找,找到即返回
- 如果一直到原型鏈的末端還沒有找到,則返回
undefined
值類型成員寫入(實例對象.值類型成員 = xx
):
- 當實例期望重寫原型對象中的某個普通數據成員時實際上會把該成員添加到自己身上
- 也就是說該行為實際上會屏蔽掉對原型對象成員的訪問
引用類型成員寫入(實例對象.引用類型成員 = xx
):
- 同上
復雜類型修改(實例對象.成員.xx = xx
):
- 同樣會先在自己身上找該成員,如果自己身上找到則直接修改
- 如果自己身上找不到,則沿著原型鏈繼續查找,如果找到則修改
- 如果一直到原型鏈的末端還沒有找到該成員,則報錯(
實例對象.undefined.xx = xx
)
更簡單的原型語法
我們注意到,前面例子中每添加一個屬性和方法就要敲一遍 Person.prototype
。
為減少不必要的輸入,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {type: 'human',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '歲了')}
}
在該示例中,我們將 Person.prototype
重置到了一個新的對象。
這樣做的好處就是為 Person.prototype
添加成員簡單了,但是也會帶來一個問題,那就是原型對象丟失了 constructor
成員。
所以,我們為了保持 constructor
的指向正確,建議的寫法是:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {constructor: Person, // => 手動將 constructor 指向正確的構造函數type: 'human',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '歲了')}
}
原生對象的原型
所有函數都有 prototype 屬性對象。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- …
原型對象的問題
- 共享數組
- 共享對象
如果真的希望可以被實例對象之間共享和修改這些共享數據那就不是問題。但是如果不希望實例之間共享和修改這些共享數據則就是問題。
一個更好的建議是,最好不要讓實例之間互相共享這些數組或者對象成員,一旦修改的話會導致數據的走向很不明確而且難以維護。
原型對象使用建議
- 私有成員(一般就是非函數成員)放到構造函數中
- 共享成員(一般就是函數)放到原型對象中
- 如果重置了
prototype
記得修正constructor
的指向
prototype 與 __proto__
prototype
每個函數都有一個prototype屬性,該屬性是一個指針,指向一個對象(構造函數的原型對象) ,這個對象包含所有實例共享的屬性和方法。原型對象都有一個constructor
屬性,這個屬性指向所關聯的構造函數。使用這個對象的好處就是可以讓所有實例對象共享它所擁有的屬性和方法。這個屬性只用js中的類(或者說能夠作為構造函數的對象)才會有。
__proto__
每個實例對象都有一個proto屬性,用于指向構造函數的原型對象(protitype
)。__proto__屬性是在調用構造函數創建實例對象時產生的。該屬性存在于實例和構造函數的原型對象之間,而不是存在于實例與構造函數之間。
function Person(name, age, job){ this.name = name;this.age = age;this.job = job;this.sayName = function(){console.log(this.name);}; // 與聲明函數在邏輯上是等價的
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
console.log(person1);
console.log(Person);
console.log(person1.prototype);//undefined
console.log(person1.__proto__);
console.log(Person.prototype);
console.log(person1.__proto__ === Person.prototype);//true1、調用構造函數創建的實例對象的prototype屬性為"undefined",構造函數的prototype是一個對象。
2、proto屬性是在調用構造函數創建實例對象時產生的。
3、調用構造函數創建的實例對象的proto屬性指向構造函數的prototype,本質上就是繼承構造函數的原型屬性。
4、在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。
__proto__
:是對象
就會有這個屬性(強調是對象);函數
也是對象,那么函數也有這個屬性咯,它指向構造函數
的原型對象;prototype
:是函數
都會有這個屬性(強調是函數),普通對象
是沒有這個屬性的(JS 里面,一切皆為對象,所以這里的普通對象
不包括函數對象
).它是構造函數的原型對象;constructor
:這是原型對象
上的一個指向構造函數
的屬性。
總結:
- 每一個對象都有
__proto__
屬性,__proto__
==>Object.prototype
(Object 構造函數的原型對象); - 每個函數都
__proto__
和prototype
屬性; - 每個
原型對象
都有constructor
和__proto__
屬性,其中constructor
指回’構造函數’, 而__proto__
指向Object.prototype
; object
是有對象的祖先,所有對象都可以通過__proto__
屬性找到它;Function
是所有函數的祖先,所有函數都可以通過__proto__
屬性找到它;- 每個函數都有一個
prototype
,由于prototype
是一個對象,指向了構造函數的原型對象 - 對象的
__proto__
屬性指向原型
,__proto__
將對象和原型鏈接起來組成了原型鏈
isPrototypeOf()
當現實中無法訪問到proto,但可以通過 isPrototypeOf()方法來確定對象之間是否存在這種關系。
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
Object.getPrototypeOf()
在所有支持的實現中,這個方法返回proto的值。例如:
//這里的person1是下面實例
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas" person1
注意: 雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性。
hasOwnProperty()
可以檢測一個屬性是存在于實例中,還是存在于原型中。返回值為true表示該屬性存在實例對象中,其他情況都為false。
function Pig(name = '佩奇', age = 1) {this.name = name;this.age = age;}Pig.prototype.sex = 1;let pig = new Pig('佩奇', 2);for (let key in pig) {if (pig.hasOwnProperty(key)) {//判斷key是否是pig的自身屬性而不是原型上屬性 有這條判斷就不會訪問到sex屬性console.log(key, pig[key]); }}
in 操作符
無論該屬性存在于實例中還是原型中。只要存在對象中,都會返回true。但是可以同時使用 hasOwnProperty()方法和 in 操作符,就可以確定該屬性到底是存在于對象中,還是存在于原型中。
function Person(name = '甲', age = 1, job = '無業'){ this.name = name;this.age = age;this.job = job;this.sayName = function(){console.log(this.name);}; // 與聲明函數在邏輯上是等價的
}var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
person1.name = "Greg";
console.log(person1.name); //"Greg" —— 來自實例
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
console.log(person2.name); //"甲" —— 來自原型
console.log(person2.hasOwnProperty("name")); //true
console.log("name" in person2); //true
delete person1.name;
console.log(person1.name); //undefined 被刪除了
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //false
用in或object[key]判斷一個對象有沒有這個屬性兩個方法的區別
let obj = {a: 1,b: undefined,c: 0}console.log(!!obj['a'], 'a' in obj);//true trueconsole.log(!!obj['b'], 'b' in obj);//false true 存在隱式轉換,所以建議用in來判斷一個對象有沒有存在的屬性console.log(!!obj['c'], 'c' in obj);//false true
原型鏈與繼承
對于使用過基于類的語言 (如 Java 或 C++) 的開發人員來說,JavaScript 有點令人困惑,因為它是動態的,并且本身不提供一個
class
實現。(在 ES2015/ES6 中引入了class
關鍵字,但那只是語法糖,JavaScript 仍然是基于原型的)。當談到繼承時,JavaScript 只有一種結構:對象。每個實例對象( object )都有一個私有屬性(稱之為 proto )指向它的構造函數的原型對象(prototype )。該原型對象也有一個自己的原型對象( proto ) ,層層向上直到一個對象的原型對象為
null
。根據定義,null
沒有原型,并作為這個原型鏈中的最后一個環節。幾乎所有 JavaScript 中的對象都是位于原型鏈頂端的
Object
的實例。盡管這種原型繼承通常被認為是 JavaScript 的弱點之一,但是原型繼承模型本身實際上比經典模型更強大。例如,在原型模型的基礎上構建經典模型相當簡單。
console.log(Function.prototype.__proto__ === Object.prototype);//true //Function的prototype是Object的實例化 //所有的對象都是Object的實例化,所以Function.prototype是一個對象,是Object的實例化
console.log(Object.__proto__ === Function.prototype);//true Object是Function的實例化
console.log(Function.__proto__ === Function.prototype);//true Function 函數對象是由它本身創建的, 所以Function的__proto__ 就是Function的prototype
console.log(Object.prototype.__proto__ === null);//true
1. 原型鏈繼承
基本思想: 利用原型讓一個引用類型繼承另一個引用類型的屬性和方法
核心:原型鏈對象 變成 父類實例,子類就可以調用父類方法和屬性。
function Parent() {
}
Parent.prototype.age = 18
Parent.prototype.getName = function () {return this.name
}function Child(name) {this.name = name
}
Child.prototype = new Parent()var child = new Child('leo')
// 這樣子類就可以調用父類的屬性和方法
console.log(child.getName()) // leo
console.log(child.age) // 18
優點: 實現簡單。
缺點:
- 引用類型值的原型屬性會被所有實例共享。
- 不能向父類傳遞參數。
function Parent() {this.likeFood = ['水果', '雞', '烤肉']
}
Parent.prototype.age = 18
Parent.prototype.getName = function () {return this.name
}function Child(name) {this.name = name
}
Child.prototype = new Parent()var chongqiChild = new Child('重慶孩子')
var guangdongChild = new Child('廣東孩子')// 重慶孩子還喜歡吃花椒。。。
chongqiChild.likeFood.push('花椒')
console.log(chongqiChild.likeFood) // ["水果", "雞", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "雞", "烤肉", "花椒"]
這時,會發現明明只是 重慶孩子 愛吃花椒,廣東孩子 莫名奇妙得也變得愛吃了????這個共享是存在問題的,不科學的。(可能重慶孩子和廣東孩子一起黑臉問號。。。)
至于第二個問題,其實也顯而易見了,沒有傳遞參數的途徑。因此,第二種繼承方式出來啦。
2. 借用構造函數繼承
遺留問題:
- 父類引用屬性共享。
- 不能傳參數到父類。
核心:子類構造函數內部調用父類構造函數,并傳入 this指針。
// 2. 借用構造函數
function Parent(name) {this.name = namethis.likeFood = ["水果", "雞", "烤肉"]
}
function Child(name) {Parent.call(this, name)
}
Parent.prototype.getName = function() {return this.name
}
var chongqingChild = new Child('重慶孩子')
var guangdongChild = new Child('廣東孩子')
chongqingChild.likeFood.push('花椒')console.log(chongqingChild.likeFood) // ["水果", "雞", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "雞", "烤肉"]
console.log(chongqingChild.name) // "重慶孩子"
console.log(chongqingChild.getName()) // Uncaught TypeError: chongqingChild.getName is not a function
值得慶幸的是,這次只有我們 重慶孩子 喜歡吃花椒,廣東孩子 沒被標記愛吃花椒啦。并且,我們通過 call 方法將我們的參數也傳入到了父類,解決了之前的遺留問題啦。
但是,原型鏈繼承 是可以調用父類方法的,但是借用構造函數卻不可以了,這是因為 當前子類的原型鏈并不指向父類了。因此,結合 第一,第二種繼承方式,第三種繼承方式應運而生啦。
3. 組合繼承
核心: 前兩者結合,進化更高級。
function Parent(name) {this.name = namethis.likeFood = ["水果", "雞", "烤肉"]
}
function Child(name, age) {Parent.call(this, name)this.age = age
}
Parent.prototype.getName = function() {return this.name
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
Child.prototype.getAge = function() {return this.age
}var chongqingChild = new Child('重慶孩子', 18)
var guangdongChild = new Child('廣東孩子', 19)
chongqingChild.likeFood.push('花椒')console.log(chongqingChild.likeFood) // ["水果", "雞", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "雞", "烤肉"]
console.log(chongqingChild.name) // "重慶孩子"
console.log(chongqingChild.getName()) // "重慶孩子"
console.log(chongqingChild.getAge()) // 18
這樣:
- 原型引用類型傳參共享問題
- 傳參問題
- 調用父類問題都解決啦。
- Javascript 的經典繼承。
- 但是有一個小缺點:在給 Child 原型賦值會執行一次Parent構造函數。所以,無論什么情況下都會調用兩次父類構造函數
4. 原型式繼承
這是在2006年一個叫 道格拉斯·克羅克福德 的人,介紹的一種方法,這種方法并沒有使用嚴格意義上的構造函數。
他的想法是 借助原型可以基于已有的對象創建新對象,同時還不必因此創建自定義類型。
這之前的三種繼承方式,我們都需要自己寫自定義函數(例如,Parent和Child)。假如,現在已經有一個對象了,并且,我也只是想用你的屬性,不想搞得那么麻煩的自定義很多函數。那怎么辦呢?
核心: 我們需要創建一個臨時的構造函數,并將作為父類的對象作為構造函數的原型,并返回一個新對象。
/*@function 實現繼承 函數@param parent 充當父類的對象
*/
function realizeInheritance(parent) {// 臨時函數function tempFunc() {}tempFunc.prototype = parentreturn new tempFunc()
}
核心點說了,我們來嘗試一下。
// 這個就是已有的對象
var baba = {name: "爸爸",likeFoods: ["水果", "雞", "烤肉"]
}
/*var newChild = {} <==> baba 這兩個對象建立關系就是這種繼承的核心了。
*/
var child1 = realizeInheritance(baba)
var child2 = realizeInheritance(baba)
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) // ["水果", "雞", "烤肉", "花椒"]
console.log(child2.likeFoods) // ["水果", "雞", "烤肉", "花椒"]
我們可以發現,父類的屬性對于子類來說都是共享的。所以,如果我們只是想一個對象和另一個對象保持一致,這將是不二之選。
ES5 新增了個 Object.create(parentObject) 函數來更加便捷的實現上述繼承
var baba = {name: "爸爸",likeFoods: ["水果", "雞", "烤肉"]
}
var child1 = Object.create(baba)
var child2 = Object.create(baba)
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) // ["水果", "雞", "烤肉", "花椒"]
console.log(child2.likeFoods) // ["水果", "雞", "烤肉", "花椒"]
效果和上面相同~
5. 寄生式繼承
這種繼承是基于原型式繼承,是同一個人想出來的,作者覺得,這樣不能有子類的特有方法,似乎不妥。就用來一個種工廠模式的方式來給予子類一些獨特的屬性。
function realizeInheritance(parent) {// 臨時函數function tempFunc() {}tempFunc.prototype = parentreturn new tempFunc()
}
// Parasitic: 寄生的 inheritance: 繼承 一個最簡單的工廠函數。
function parasiticInheritance(object) {var clone = realizeInheritance(object) // 這是用了原型式繼承,但是只要是任何可以返回對象的方法都可以。clone.sayName = function() {console.log('我是'+this.name)}return clone
}
var baba = {name: "爸爸",likeFoods: ["水果", "雞", "烤肉"]
}
var child = parasiticInheritance(baba)
child.name = '兒子'
child.sayName() // 我是兒子
缺點:使用寄生式繼承來為對象添加函數,會由于不能做到函數復用而降低效率(每一個函數都是新的);這一點與構造函數繼承類似。
6.寄生組合式繼承
我們先回顧之前的 組合繼承
function Parent(name) {this.name = namethis.likeFood = ["水果", "雞", "烤肉"]
}
function Child(name, age) {Parent.call(this, name) // 第二次調用this.age = age
}
Parent.prototype.getName = function() {return this.name
}
Child.prototype = new Parent() // 第一次調用
Child.prototype.constructor = Child
Child.prototype.getAge = function() {return this.age
}
這個兩次調用的問題之前有提及過。過程大致:
- 第一次調用,Child 的原型被賦值了 name 和 likeFood 屬性
- 第二次調用,注入this,會在Child 的實例對象上注入 name 和 likeFood 屬性,這樣就屏蔽了原型上的屬性。
只要了問題,我們就來解決這個問題~
function Parent(name) {this.name = namethis.likeFood = ["水果", "雞", "烤肉"]
}
function Child(name, age) {Parent.call(this, name)this.age = age
}
Parent.prototype.getName = function() {return this.name
}// Child.prototype = new Parent() 使用新方法解決
// Child.prototype.constructor = Child
inheritPrototype(Child, Parent)
function inheritPrototype(childFunc, parentFunc) {var prototype = realizeInheritance(parentFunc.prototype) //創建對象,我們繼續是用原型式繼承的創建prototype.constructor = childFunc //增強對象childFunc.prototype = prototype //指定對象
}
function realizeInheritance(parent) {// 臨時函數function tempFunc() {}tempFunc.prototype = parentreturn new tempFunc()
}Child.prototype.getAge = function() {return this.age
}var chongqingChild = new Child('重慶孩子', 18)
var guangdongChild = new Child('廣東孩子', 19)
chongqingChild.likeFood.push('花椒')console.log(chongqingChild.likeFood) // ["水果", "雞", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "雞", "烤肉"]
console.log(chongqingChild.name) // "重慶孩子"
console.log(chongqingChild.getName()) // "重慶孩子"
console.log(chongqingChild.getAge()) // 18
這種方法的核心思想:
- 首先,用一個空對象建立和父類關系。
- 然后,再用這個空對象作為子類的原型對象。
這樣,中間的對象就不存在new 構造函數的情況(這個對象本來就沒有自定義的函數),這樣就避免了執行構造函數,這就是高效率的體現。并且,在中間對象繼承過程中,父類構造器也沒有執行。所以,沒有在子類原型上綁定屬性。
這種繼承方式也被開發人員普遍認為是引用類型最理想的繼承范式。
總結
- 模式(簡述):
- 工廠模式:創建中間對象,給中間對象賦添加屬性和方法,再返回出去。
- 構造函數模式:就是自定義函數,并用過 new 關鍵子創建實例對象。缺點也就是無法復用。
- 原型模式: 使用 prototype 來規定哪一些屬性和方法能被共享。
- 繼承
- 原型鏈繼承:
- 優點:只調用一次父類構造函數,能復用原型鏈屬性
- 缺點:部分不想共享屬性也被共享,無法傳參。
- 構造函數繼承:
- 優點:可以傳參,同屬性可以不被共享。
- 缺點:無法使用原型鏈上的屬性
- 組合繼承
- 優點:可以傳參,同屬性可以不被共享,能使用原型鏈上的屬性。
- 缺點:父類構造函數被調用2次,子類原型有冗余屬性。
- 原型式繼承:(用于對象與對象之間)
- 優點:在對象與對象之間無需給每個對象單獨創建自定義函數即可實現對象與對象的繼承,無需調用構造函數。
- 缺點:父類屬性被完全共享。
- 寄生式繼承:
- 優點:基于原型式繼承僅僅可以為子類單獨提供一些功能(屬性),無需調用構造函數。
- 缺點:父類屬性被完全共享。
- 寄生組合繼承:
- 優點:組合繼承+寄生式繼承,組合繼承缺點在于調用兩次父類構造函數,子類原型有冗余屬性,寄生式繼承的特性規避了這類情況,集寄生式繼承和組合繼承的優點與一身,是實現基于類型繼承的最有效方式。
- 原型鏈繼承:
Object.create()
**
Object.create()
**方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。
const person = {isHuman: false,printIntroduction: function() {console.log(`我的名字是 ${this.name}. 我是人嗎? ${this.isHuman}`);}
};const me = Object.create(person);me.name = 'Matthew';
me.isHuman = true; me.printIntroduction();
// "我的名字是 Matthew. 我是人嗎? true"
用于原型繼承
function Animal(name, age) {this.name = name;this.age = age;}Animal.prototype.showName = function () {console.log(this.name, `我是${this.constructor.name}類`);}Animal.prototype.showAge = function () {console.log(this.age, `我是${this.constructor.name}類`);}function Pig(name, age, sex = "公") {Animal.call(this, name, age);this.sex = sex;}Pig.prototype = Object.create(Animal.prototype);Pig.prototype.constructor = Pig;Pig.prototype.showSex = function () {console.log(this.sex, `我是${this.constructor.name}類`);}let pig = new Pig('佩奇', 1, '母');console.log(pig);//Pig {name: "佩奇", age: 1, sex: "母"}pig.showName(); //佩奇pig.showAge(); //1pig.showSex(); //母
多重繼承
function Parent1(name) {this.name = name;}Parent1.prototype.showName = function () {console.log(this.name)}Parent1.prototype.showAge = function () {console.log(this.age)}function Parent2(age) {this.age = age;}Parent2.prototype.showSomething = function () {console.log('something')}function Child(name, age, address) {Parent1.call(this, name);Parent2.call(this, age);this.address = address;}function mixProto(targetClass, parentClass, otherParent) {targetClass.prototype = Object.create(parentClass.prototype);Object.assign(targetClass.prototype, otherParent.prototype);}mixProto(Child, Parent1, Parent2)var child = new Child('佩奇', 3, '火星');console.log(child); //Child {name: "佩奇", age: 3, address: "火星"}child.showName();//佩奇child.showAge();//3child.showSomething(); //something
繼承常用方式
function Pig(name, age) {this.name = name;this.age = age;}Pig.rotate = 10; //直接掛載在Pig上的靜態方法和屬性是不會被繼承的,靜態方法只能通過類來調用,實例無法調用Pig.staticShowMe = function () {//注意,如果靜態方法包含`this`關鍵字,這個`this`指的是類,而不是實例 console.log('我是Pig的靜態方法 ')}Pig.prototype.showName = function () { console.log(this.name)}//大多數繼承兩件套寫法function Spig(name, age, sex) {Pig.call(this, name, age);//屬性的繼承this.sex = sex;//特有屬性}Spig.prototype = Object.create(Pig.prototype);//只繼承原型上的方法let pig = new Spig('xxxxx', 1, 0);pig.showName();
原型案例
創建可移動對象
<!DOCTYPE html>
<html lang="zh-cn">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>私有屬性 </title><style>* {margin: 0;padding: 0;}.wrap {width: 50vw;height: 80vh;background-color: #ccc;}img {position: absolute;top: 0;left: 0;}</style>
</head>
<body><div class="wrap"></div><script src="js/pig.js"></script><script>let pig = new Pig({ name: '佩奇', age: 2, weight: 888, pic: "images/p1.jpg", container: ".wrap" });let pig2 = new Pig({ name: '佩奇2', age: 2, weight: 888, pic: "images/p1.jpg", container: ".wrap" });</script>
</body>
</html>
(function (w) {const PI = 3.14;function Pig (param = {}) {this.init(param);}Pig.dragMap = {'mousedown': function (e, pig) {let ele = pig.ele.dom;let pos = pig.ele.pospos.x = e.clientX;pos.y = e.clientY;pos.left = ele.offsetLeft;pos.top = ele.offsetTop;pig.ele.isDown = true;},'mousemove': function (e, pig) {if (pig.ele.isDown) {let ele = pig.ele.dom;let pos = pig.ele.pos;let _x = e.clientX - pos.x;let _y = e.clientY - pos.y;ele.style.left = _x + pos.left + 'px';ele.style.top = _y + pos.top + 'px';}},'mouseup': function (e, pig) {pig.ele.isDown = false;}};Pig.prototype.siblings = [];//Array 引用類型Pig.prototype.init = function ({ name = 'p1', age = 1, weight = 100, pic = "images/pig.jpg", container = '.container' }) {this.name = name;this.age = age;this.weight = weight;this.picUrl = pic;this.container = document.querySelector(container);this.size = this.container.offsetWidth / 10;this.siblings.push(this);this.draw();this.dragInit();}Pig.prototype.draw = function () {const vDom = document.createElement('img');vDom.src = this.picUrl;vDom.width = this.size;this.ele = {dom: vDom,pos: {x: 0,y: 0,left: 0,top: 0},isDown: false}this.container.appendChild(vDom);}Pig.prototype.dragInit = function () {let eleImg = this.ele.dom;let drag = (e) => {e.preventDefault();//this 指向 監聽事件的DOM對象if (Pig.dragMap[e.type]) {Pig.dragMap[e.type](e, this);}return false;}eleImg.addEventListener('mousedown', drag, false);w.document.addEventListener('mousemove', drag, false);w.document.addEventListener('mouseup', drag, false);}Pig.prototype.eat = function () {console.log(`我是${this.name} 我${this.age}歲了 我要吃飯了`);}Pig.prototype.bloodReturn = function (len) {//根據一共實例化了多少頭豬來確定回多少血console.log(`回血 ${len * 100}`);}//料肉比 飼料/增肉量 Pig.prototype.feedConversionRatio = 2.4;w.Pig = Pig;//Pig構造函數掛載在 全局window對象上
})(window);
Object 深入
Object方法
Object.getOwnPropertyNames()
//方法返回一個由指定對象的所有自身屬性的屬性名(包括不可枚舉屬性但不包括Symbol值作為名稱的屬性)組成的數組。
let arr = [1, 2, 3];
Object.getOwnPropertyNames(arr)//['0', '1', '2', 'length']
Object.keys(arr)// Object.keys是返回可枚舉屬性['0', '1', '2']Object.getPrototypeOf()
//方法返回指定對象的原型(內部[[Prototype]]屬性的值)。
//如果沒辦法使用__proto__就使用這個
Object.getPrototypeOf(arr) === arr.__proto__ //trueObject.getOwnPropertyDescriptors()
//方法用來獲取一個對象的所有自身屬性的描述符。
const O = {name: 'kyogre',age: 123
};
let descriptor = Object.getOwnPropertyDescriptors(O);
console.log(descriptor);
//{age:
//{configurable: true//可配置
//enumerable: true//可枚舉
//value: 123
//writable: true}//可寫
//name:
//{configurable: true
//enumerable: true
//value: "kyogre"
//writable: true}}Object.getOwnPropertyDescriptor()
//方法返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性)
//console.log(Object.getOwnPropertyDescriptor(O,'name'))
//{value: 'kyogre', writable: true, enumerable: true, configurable: true}Object.assign()
//方法用于將所有可枚舉屬性的值從一個或多個源對象復制到目標對象。它將返回目標對象,不會合并__proto__不可枚舉對象
//合并的對象同名屬性會被覆蓋,數組也可以合并,數組的下標作為對象的鍵Object.create()
//方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。 (請打開瀏覽器控制臺以查看運行結果。)Object.freeze()
//方法可以凍結一個對象。一個被凍結的對象再也不能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象后該對象的原型也不能被修改。freeze() 返回和傳入的參數相同的對象。
//被凍結后打印的對象的屬性描述符中configurable: false不可配置,也不可寫入writable: falseObject.isFrozen()
//方法判斷一個對象是否被凍結。Object.isSealed()
//方法判斷一個對象是否被密封。hasOwnProperty()
//方法會返回一個布爾值,判斷一個對象的屬性是否為自身的屬性而非原型上的屬性
O.hasOwnProperty('name')//true
O.hasOwnProperty('valueOf')//false 原型屬性
O.hasOwnProperty('a')//false 屬性不存在也是falseisPrototypeOf()
//方法用于測試一個對象是否存在于另一個對象的原型鏈上。Object.is()
//方法判斷兩個值是否為同一個值。相當于===。指針不一致也是false
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(Object.is(arr1, arr2));//false
let arr1 = [1, 2, 3];
let arr2 = arr1;
console.log(Object.is(arr1, arr2));//true
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,并返回此對象。
語法
Object.defineProperty(obj, prop, descriptor)
參數
-
obj
要定義屬性的對象。
-
prop
要定義或修改的屬性的名稱或
Symbol
。 -
descriptor
要定義或修改的屬性描述符。
返回值
被傳遞給函數的對象。
該方法允許精確地添加或修改對象的屬性。通過賦值操作添加的普通屬性是可枚舉的,在枚舉對象屬性時會被枚舉到(for...in 或 Object.keys 方法),可以改變這些屬性的值,也可以刪除這些屬性。這個方法允許修改默認的額外選項(或配置)。默認情況下,使用 Object.defineProperty() 添加的屬性值是不可修改(immutable)的。對象里目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個具有值的屬性,該值可以是可寫的,也可以是不可寫的。存取描述符是由 getter 函數和 setter 函數所描述的屬性。一個描述符只能是這兩者其中之一;不能同時是兩者。這兩種描述符都是對象。它們共享以下可選鍵值(默認值是指在使用 Object.defineProperty() 定義屬性時的默認值):
-
configurable
當且僅當該屬性的
configurable
鍵值為true
時,該屬性的描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除。 默認為false
,為false時,即使用delete也無法刪除對象的屬性。 -
enumerable
當且僅當該屬性的
enumerable
鍵值為true
時,該屬性才會出現在對象的枚舉屬性中。 默認為false
。
數據描述符還具有以下可選鍵值:
-
value
該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。 默認為
undefined
。 -
writable
當且僅當該屬性的
writable
鍵值為true
時,屬性的值,也就是上面的value
,才能被賦值運算符
改變。 默認為false
。
存取描述符還具有以下可選鍵值:
-
get
屬性的 getter 函數,如果沒有 getter,則為
undefined
。當訪問該屬性時,會調用此函數。執行時不傳入任何參數,但是會傳入this
對象(由于繼承關系,這里的this
并不一定是定義該屬性的對象)。該函數的返回值會被用作屬性的值。 默認為undefined
。 -
set
屬性的 setter 函數,如果沒有 setter,則為
undefined
。當屬性值被修改時,會調用此函數。該方法接受一個參數(也就是被賦予的新值),會傳入賦值時的this
對象。 默認為undefined
。
描述符默認值匯總
- 擁有布爾值的鍵
configurable
、enumerable
和writable
的默認值都是false
。 - 屬性值和函數的鍵
value
、get
和set
字段的默認值為undefined
。
描述符可擁有的鍵值
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
數據描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
如果一個描述符不具有 value
、writable
、get
和 set
中的任意一個鍵,那么它將被認為是一個數據描述符。如果一個描述符同時擁有 value
或 writable
和 get
或 set
鍵,則會產生一個異常。
記住,這些選項不一定是自身屬性,也要考慮繼承來的屬性。為了確認保留這些默認值,在設置之前,可能要凍結 Object.prototype
,明確指定所有的選項,或者通過 Object.create(null)
將 __proto__
屬性指向 null
。
Object.defineProperty()
可以對單條屬性進行修改
let o = {name: 'kyogre',age: 13}Object.defineProperty(o, 'age', {configurable: true, //可配置enumerable: true, //可枚舉value: 19, //屬性值writable: true,//可寫入});
Object.defineProperties()
可以對多條屬性進行修改
let o = {name: 'kyogre',age: 13}Object.defineProperties(o, {"name": {configurable: true, //可配置enumerable: true, //可枚舉value: 19, //屬性值writable: true,//可寫入},"age": {configurable: true, //可配置enumerable: true, //可枚舉value: 19, //屬性值writable: true,//可寫入}})
Object.defineProperty()
的封裝
//使用get()、set()設置及獲取數據的封裝let O = {name: 'o'}defineReactive(O, 'name', 'o');function defineReactive(obj, key, val{Object.defineProperty(obj, key, {set(newValue) {value = newValue;},get() {return value;}});}O.name = '大笨蛋';console.log(O.name)//'大笨蛋'
Object.entries()
方法返回一個給定對象自身可枚舉屬性的鍵值對數組,其排列與使用
for...in
循環遍歷該對象時返回的順序一致(區別在于 for-in 循環還會枚舉原型鏈中的屬性)。
語法
Object.entries(obj)
參數
-
obj
可以返回其可枚舉屬性的鍵值對的對象。
返回值
? 給定對象自身可枚舉屬性的鍵值對數組。
const object1 = {a: 'somestring',b: 42
};console.log(Object.entries(object1));//[['a', 'somestring'],['b', 42]]
for (const [key, value] of Object.entries(object1)) {console.log(`${key}: ${value}`);
}
// "a: somestring"
// "b: 42"
案例:使用Object.entries()可以很好的將對象的屬性或屬性值返回成一個數組
let o = {t: 1,b: 2,l: 3,r: 4
}console.log(Object.entries(o).map(([key, value]) => (key)))//取鍵['t', 'b', 'l', 'r']
console.log(Object.entries(o).map(([key, value]) => (value)))// 取值[1, 2, 3, 4]
console.log(Object.entries(o).map(([key, value]) => ([key, value * 3])))//處理值后返回新數組[['t', 3],['b', 6],['l', 9],['r', 12]]//可以使用
Object.keys()
方法會返回一個由一個給定對象的自身可枚舉屬性組成的數組,數組中屬性名的排列順序和正常循環遍歷該對象時返回的順序一致 。。
語法
Object.keys(obj)
參數
-
obj
要返回其枚舉自身屬性的對象。
返回值
? 一個表示給定對象的所有可枚舉屬性的字符串數組。
const arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']// array like object
const obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
Object.fromEntries()
方法把鍵值對列表轉換為一個對象。
語法
Object.fromEntries(iterable);
參數
-
iterable
類似
Array
、Map
或者其它實現了可迭代協議的可迭代對象。
返回值
一個由該迭代對象條目提供對應屬性的新對象
const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }
Object.preventExtensions()
方法讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性,只能對已有屬性進行操作,如刪除或修改已有屬性。
語法
Object.preventExtensions(obj)
參數
-
obj
將要變得不可擴展的對象。
返回值
已經不可擴展的對象。
var obj = {};
var obj2 = Object.preventExtensions(obj);
obj === obj2; // true// 字面量方式定義的對象默認是可擴展的.
var empty = {};
Object.isExtensible(empty) //=== true// ...但可以改變.
Object.preventExtensions(empty);
Object.isExtensible(empty) //=== false// 使用Object.defineProperty方法為一個不可擴展的對象添加新屬性會拋出異常.
var nonExtensible = { removable: true };
Object.preventExtensions(nonExtensible);
Object.defineProperty(nonExtensible, "new", { value: 8675309 }); // 拋出TypeError異常// 在嚴格模式中,為一個不可擴展對象的新屬性賦值會拋出TypeError異常.
function fail()
{"use strict";nonExtensible.newProperty = "FAIL"; // throws a TypeError
}
fail();const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }
深拷貝
深拷貝:就是完完全全拷貝一份新的對象,它會在內存的堆區域重新開辟空間,修改拷貝對象就不會影響到源對象。
淺拷貝:拷貝基本數據類型時,不受任何影響,當拷貝引用類型時,源對象也會被修改。
JSON.parse(JSON.stringify(待拷貝對象))
缺點:沒辦法拷貝內部函數,也沒辦法拷貝正則對象
let a = {name : '張三',age : '18',a:/^a$/,like(){console.log('喜歡唱歌、滑冰');}
}
let b =JSON.parse( JSON.stringify(a) );
b.name = '李四';
console.log('a:',a);
//a:{name: '張三', age: '18', a: /^a$/, like: ? like()}
console.log('b:',b);
//b:{name: '李四', age: '18', a: {}}let a = {name : '張三',age : '18',a:/^a$/,like(){console.log('喜歡唱歌、滑冰');}
}
let b = {...a};
b.name = '李四';
console.log('a:',a);//{name: '張三', age: '18', a: /^a$/, like: ?}
console.log('b:',b);//{name: '李四', age: '18', a: /^a$/, like: ?}
es6的展開語法拷貝
缺點:即只能深拷貝第一層,對于多層拷貝無效
js中針對數組Array的slice和concat方法,也是只能拷貝第一層
const a = {arr: [1, 2, " % E4 % BD % A0 % E5 % A5 % BD", { x: 1, y: [1, 2, 3] }],json: {key: [1, 2, 3, 4],value: {a: null,b: undefined,c: function () {console.log('c');}}},name: 'oooo'
}
let b = {...a};b.arr[3].x = '222222';
console.log('a:',a);
console.log('b:',b);//a和b的arr[3].x一起改變為222222
遞歸實現深拷貝
const a = {arr: [1, 2, " % E4 % BD % A0 % E5 % A5 % BD", { x: 1, y: [1, 2, 3] }],json: {key: [1, 2, 3, 4],value: {a: null,b: undefined,c: function () {console.log('c');}}},name: 'oooo'}function deepCopy(original) {if (Array.isArray(original)) {return original.map(ele => deepCopy(ele));}if (typeof original === 'object' && original !== null) {return Object.fromEntries(Object.entries(original).map(([key, value]) => [key, deepCopy(value)]));}else {return original;}}const b = deepCopy(a)b.arr[3].x = '222222';console.log('a:',a);console.log('b:',b);
遞歸函數
前進階段(遞歸階段)
終止階段(返回階段)
必須有結束條件(遞歸邊界)
遞歸練習
- 找寶藏
? 找到對象 o里面的屬性[[[c]]]的值為 ‘恭喜你’,找到后返回 ‘恭喜你找到寶藏’
let o = {c: {c: {c: {c: {c: '恭喜你'}}}}}let count = 0;function findResult(o) {if (o['c'] !== '恭喜你') {count++;return findResult(o['c']); //前進}return '恭喜你找到寶藏'; //終止}console.log(findResult(o));console.log(count);
- 1-100的和
let result = (1 + 100) * (100 / 2);console.log(result);let c = 0;for (let i = 1; i <= 100; i++) {c += i;}console.log(c);console.log(getCountNum(100));function getCountNum(n) {if (n === 1) return 1;return n + getCountNum(n - 1);}
- 編寫一個函數實現n^k (n的k次方),使用遞歸實現
function getPow(n, k) {k--;if (k === 0) {//出口條件return n;}//前進條件return n * getPow(n, k);}console.log(getPow(3, 5));
-
角谷定理。輸入一個自然數,若為偶數,則把它除以2,若為奇數,則把它乘以3加1。經過如此有限次運算后,總可以得到自然數值1。求經過多少次可得到自然數1。
function fn1(n, c = 0) {c++;if (n === 2) {//倒數第二次return c;}return fn1(((n % 2 === 0) ? n / 2 : n * 3 + 1), c);}console.log(fn1(5));
-
一個人趕著鴨子去每個村莊賣,每經過一個村子賣去所趕鴨子的一半又一只。這樣他經過了7個村子后還剩2鴨子,問他出發時共趕多少只鴨子?經過每個村子賣出多少只鴨子?
function fn2(n, t, arr = []) { //2 ,7if (t === 0) {//終止return {first: n,arr: arr};}arr.unshift(n + 2);t--; // 6//前進return fn2(2 * (n + 1), t, arr);}console.log(fn2(2, 7));;
- 數列 0,1,1,2,4,7,13,24,44,…求數列的第 n項 (找規律) 4= 1 + 1 +2 7 = 1+2+4
function fn3(n) {if (n === 0 || n === 1) {return 1;}if (n === 2) {return 2;}return fn3(n - 3) + fn3(n - 2) + fn3(n - 1);}console.log(fn3(2))