面向對象
面向對象編程(Object Oriented Programming , OOP) 是一種編程範式,它將代碼分爲具有屬性和方法的對象。這種方式的好處是:將相關代碼片段封裝到對象中,由對象來維護在程序裏面的生命狀態。對象可以按需被複用或者被修改。
編程範式
在此之前呢先介紹一下,什麼叫做編程範式。
所謂編程範式(programming paradigm),指的是計算機編程的基本風格或典範模式。借用哲學的屬於,如果說每一個編程者都在創造虛擬世界,那麼編程範式就是他們置身其中自覺不自覺採用的世界觀和方法論。
在我們的編程語言裏面,根據編程範式來分類大致可以分爲兩大類:命令式編程和聲明式編程
1.命令式編程
一直以來,比較流行的都是命令式編程。所謂命令式編程,就是以命令爲主,給機器提供一條又一條的命令序列讓其原封不動的執行。程序執行的效率取決於執行命令的數量。
一句話概括:命令式編程就是命令**"機器"如何去做事情,這樣不管你想要的是什麼(what),它都會按照你的命令實現。**
我們常見的命令式編程有C語言,C++,Java,C#
2.聲明式編程
所謂聲明式編程就是告訴**“機器” 你想要什麼,讓機器想出如何去做(how)**
在聲明式編程裏面又可以分爲2個大類:領域專用語言和函數式編程。
領域專用語言:
英語全稱爲 domain specific language ,簡稱SDL。主要是指對一些對應專門領域的高層編程語言,和通用編程語言的概念相對。DSL對應的專門領域(Domain)一般比較狹窄,或者對應某個行業,或者對於某一類具體應用程序,比如數據庫等。我們常見的有HTML、CSS、SQL等。
函數式編程:
所謂函數式編程,簡單來講,就是一種編程模型,將計算機運算看作是數學中函數的計算。在函數式編程中,函數是基本單位,是第一型,它幾乎被用做一切,包括最簡單的計算,甚至連變量都別計算索取代。在函數式編程中,變量只是一個名稱,而不是一個存儲單元,這是函數式編程與傳統的命令式編程最典型的不同之處。
隨着web開發中高併發的需求越來越多,多核並行的程序涉及被推倒前線,而傳統的命令式編程天生的缺陷使得並行編程模型變得非常複雜,使程序員不堪其重。函數式編程在解決兵法程序上面卻有着天然的優勢,代碼簡潔優雅,但是缺點就是學習門檻高。
面向對象
一、面向對象基本特徵
- 封裝:也就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
- 繼承:通過繼承創建的新類稱爲“子類”或“派生類”。繼承的過程,就是從一般到特殊的過程。
- 多態:對象的多功能,多方法,一個方法多種表現形式。
- Javascript是一種基於對象(object-based)的語言。但是,它又不是一種真正的面向對象編程(OOP)語言,因爲它的語法中沒有class(類)—–es6以前是這樣的。所以es5只有使用函數模擬的面向對象。
二、對象實例化方式
- 原始模式:這樣的寫法有兩個缺點,一是如果多生成幾個(100個!)實例,寫起來就非常麻煩;二是實例與原型之間,沒有任何辦法,可以看出沒有什麼聯繫。
var Car = {
color: 'red',//車的顏色
wheel: 4,//車輪數量
}
var Car2 = {
color: 'blue',
wheel: 4,
}
alert(Car.color);//red
- 原始模式的改進:通過寫一個函數,解決代碼重複的問題。
function createCar(color,wheel) {
return {
color:color,
wheel:wheel
}
}
//然後生成實例對象,就等於是在調用函數:
var cat1 = createCar("紅色","4");
var cat2 = createCar("藍色","4");
alert(cat1.color);//紅色
- 工廠模式
function createCar(color,wheel){//createCar工廠
var obj = new Object;//或obj = {} 原材料階段
obj.color = color;//加工
obj.wheel = wheel;//加工
return obj;//輸出產品
}
//實例化
var cat1 = createCar("紅色","4");
var cat2 = createCar("藍色","4");
alert(cat1.color);//紅色
- 構造函數模式:爲了解決從原型對象生成實例的問題,Javascript提供了一個構造函數(Constructor)模式。 所謂”構造函數”,其實就是一個普通函數,但是內部使用了this變量。對構造函數使用new運算符,就能生成實例,並且this變量會綁定在實例對象上。加new執行的函數構造內部變化:自動生成一個對象,this指向這個新創建的對象,函數自動返回這個新創建的對象
function CreateCar(color,wheel){//構造函數首字母大寫
//不需要自己創建對象了
this.color = color;//添加屬性,this指向構造函數的實例對象
this.wheel = wheel;//添加屬性
//不需要自己return了
}
//實例化
var cat1 = new CreateCar("紅色","4");
var cat2 = new CreateCar("藍色","4");
alert(cat1.color);//紅色
三、構造函數注意事項
- 此時CreateCar稱之爲構造函數,也可以稱之類,構造函數就是類 。
- cat1,cat2均爲CreateCar的實例對象。
- CreateCar構造函數中this指向CreateCar實例對象即 new CreateCar( )出來的對象。
- 必須帶new 。
- 構造函數首字母大寫,這是規範,官方都遵循這一個規範,如Number() Array()。
- contructor:這時cat1和cat2會自動含有一個constructor屬性,指向它們的構造函數,即CreateCar。
alert(cat1.constructor == CreateCar); //true
alert(cat2.constructor == CreateCar); //true
- 每定義一個函數,這個函數就有一個 prototype 的屬性{},proto 指向被實例化的構造函數的prototype,prototype默認帶constructor屬性,constructor指向構造函數。
instanceof 運算符:object instanceof constructor運算符,驗證構造函數與實例對象之間的關係。
alert(cat1 instanceof CreateCar ); //true
alert(cat2 instanceof CreateCar ); //true
四、構造函數的問題
構造函數方法很好用,但是存在一個浪費內存的問題。如果現在爲其再添加一個方法showWheel。那麼,CreateCar就變成了下面這樣,這樣做有一個很大的弊端,對於每一個實例對象,showWheel都是一模一樣的內容,每一次生成一個實例,都必須生成重複的內容,多佔用一些內存。這樣既不環保,也缺乏效率。
function CreateCar(color,wheel){
this.color = color;
this.wheel = wheel;
this.showWheel = function(){//添加一個新方法
alert(this.wheel);
}
}
//還是採用同樣的方法,生成實例:
var cat1 = new CreateCar("紅色","4");
var cat2 = new CreateCar("藍色","4");
alert(cat1.showWheel == cat2.showWheel); //false
五、Prototype 原型
Javascript規定,每一個構造函數都有一個prototype屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承。 這意味着,我們可以把那些不變的屬性和方法,直接定義在prototype對象上。__proto__是原型鏈,指向實例化的函數原型。
function CreateCar(color,wheel){
//屬性寫構造函數裏面
this.color = color;
this.wheel = wheel;
}
//方法寫原型裏面
CreateCar.prototype.showWheel = function(){
alert(this.wheel);
}
CreateCar.prototype.showName = function(){
alert('車');
}
//生成實例。
var cat1 = new CreateCar("紅色","4");
var cat2 = new CreateCar("藍色","4");
cat1.showName();//'車'
//這時所有實例的showWheel屬性和showName方法,其實都是同一個內存地址,指向prototype對象,因此就提高了運行效率。
alert(cat1.showWheel == cat2.showWheel );//true
alert(cat1.showName == cat2.showName );//true
console.log(cat1.__proto__ === CreateCar.prototype); //true
六、對象和函數的關係
對象是由函數構造出來的。
Object是Function 的一個實例。
Object.constructor == Function //true
函數是Function 的實例,但不是Object 的實例。
function fn(){}
fn.constructor == Function //true
fn.constructor == Object //false
{} 與 Object 的關係。
var obj = {};
obj.constructor === Object //true
七、靜態方法和靜態屬性
只屬於類而不屬於實例化對象
function foo(){
this.show = function(){
return this;
}
}
foo.test = 123; //靜態屬性
foo.say = function(){
return this;
}
foo.say();
var fn = new foo(); //實例化的新的對象,this指向這個新的對象,不能訪問類的靜態方法
fn.say(); //Noname1.html:45 Uncaught TypeError: fn.say is not a function
console.log(foo.say() == fn.say());
八、對象繼承
利用call()及for in繼承 。
給對象的constructor.prototype添加方法屬性,對象就會繼承,如果要實現一個對象繼承其他對象,採用如下方法。
//人類
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.run = function(){
console.log('跑路~')
};
Person.prototype.say = function(){
console.log('說話~')
};
console.log(Person.prototype);
//男人
function Man(){
this.sex = "男";
}
Man.prototype = Person.prototype;
Man.prototype.yyy = function(){
console.log('嚶嚶嚶');
}
//會發現Person的prototype也改變了,因爲複雜對象的賦值操作是引用而不是賦值
console.log(Person.prototype);
//人類
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.run = function(){
console.log('跑路~')
};
Person.prototype.say = function(){
console.log('說話~')
};
console.log(Person.prototype);
//男人
function Man(){
this.sex = "男";
}
for(var key in Person.prototype){
Man.prototype[key] = Person.prototype[key];
console.log(key)
}
Man.prototype.yyy = function(){
console.log('嚶嚶嚶');
}
console.log(Person.prototype);
var xm = new Man();
xm.yyy();
採用中介
function ClassA(name){
this.name = name;
}
ClassA.prototype.say = function(){
console.log(666);
}
//中繼來做準備工作
function Ready(){}//
Ready.prototype = ClassA.prototype;//引用
//需要來繼承ClassA
function ClassB(){}
ClassB.prototype = new Ready();//new 返回了一個新對象 __proto__指向被實例化的構造函數的prototype
ClassB.prototype.constructor = ClassB;
console.log(ClassB.prototype);
採用中介,使用call改變this指向
function ClassA(name){
this.name = name;
}
ClassA.prototype.showName = function(){
console.log(this.name);
}
//中繼來做準備工作
function Ready(){}//
Ready.prototype = ClassA.prototype;//引用
//需要來繼承ClassA
function ClassB(name){
ClassA.call(this,name);
}
ClassB.prototype = new Ready();//new 返回了一個新對象 __proto__指向被實例化的構造函數的prototype
ClassB.prototype.constructor = ClassB;
console.log(ClassB.prototype);
var xiaoming = new ClassB('小明');
xiaoming.showName();
九、多態
同一個方法,面對不同的對象有不同的表現形式就叫做多態。
var obj = {
eat : function(_type){
if(_type == '貓'){
console.log('貓糧')
}else if (_type == "狗") {
console.log('狗糧')
}else{
console.log("吃飯");
}
}
};
obj.eat("狗");
十、hasOwnProperty
查看該屬性是否在這個對象本身上,只有在自身屬性上纔會返回真,在原型鏈上會返回假。
function ClassA(){}
ClassA.prototype.test = function(){
console.log('test')
}
var a = new ClassA();
a.test();
console.log(a.hasOwnProperty('test')); //false
十一、描述符(修飾符)
描述符是對一個屬性的特性的描述,defineProperty設置描述符(修飾符),value設置屬性值,configurable是否允許修飾符被改變 默認爲false,enumerable 是否可以被枚舉 默認爲false,writable 是否可以被 = 等號改變 默認爲false。
var obj = {
a : 1
};
var c = 666;
Object.defineProperty(obj,'c',{
//value : 233,
//enumerable : false,
//writable : true,//他的值能否改變
//設置的時候調用
set : function(n){
//n 就是等號的右邊的值
c = c*n;
},
//獲取的時候調用
get : function(){
return c;
},
configurable : true,//是否可以再次修改修飾符
});
面向對象總結
- 所謂編程範式,指的是計算機編程的基本風格或典範模式。大致可以分爲命令式編程和聲明式編程。
- 面向對象的程序設計是站在哲學的角度上,將人類思維融入到了程序裏面的一種編程範式。
- 描述對象的時候可以通過兩個方法來進行描述。分別是對象的外觀和功能。在程序中與之對應的就是屬性和方法。
- JavaScript中的對象都是基於原型的,從一個原型對象中可以克隆出一個新的對象。
- 在JavaScript中每個對象都有一個原型對象。可以通過
__proto__
屬性來找到一個對象的原型對象。 - 在其他語言中,對象從類中產生。而JavaScript中,通過構造函數來模擬其他語言中的類。
- 類與對象的關係可以總結爲:類是對對象的一種概括,而對象是類的一種具體實現。
- 面向對象的三大特徵:封裝、繼承、多態。
- 封裝是指對象的屬性或者方法隱藏於內部,不暴露給外部。
- 繼承是指一個子類繼承一個父類。在繼承了父類之後,子類就擁有了父類的所有屬性和方法。
- 多態是指不同的對象可以擁有相同的方法,不過是以不同的方式來實現它。
- this的指向是根據使用的地方不同分爲好幾種情況,但是我們可以通過一些方式來修改this的指向。
執行上下文
- 先進後出,後進先出。
函數的執行上下文的生命週期
執行上下文的生命週期分爲兩個階段:
- 創建階段(進入執行上下文):函數被調用時,進入函數環境,爲其創建一個函數上下文
- 執行階段(代碼執行):執行函數中代碼時,此時執行上下文進入執行狀態。
創建階段的操作
- 創建變量對象
- 函數環境會初始化創建 Arguments 對象,形式參數(並賦值)。
- 普通函數聲明(並賦值)
- 局部變量聲明,函數表達式聲明(未賦值)
- 初始化作用域鏈
- 確定this指向(this由調用者確定)
- 確定作用域(詞法環境決定,哪裏聲明定義,就在哪裏確定)
執行階段的操作
- 變量對象賦值
- 變量賦值
- 函數表達式賦值
- 調用函數
- 按順序執行其他代碼
執行上下文與作用域區別
作用域和執行上下文不是同一個概念。
執行全局代碼時,會產生一個執行上下文環境,每次調用函數都會執行上下文環境。當函數調用完時,這個上下文環境以及其中的數據都會被消除(除了閉包),處於活動狀態的執行上下文環境只有一個
而作用域在函數定義時就已經確定了,不是在函數調用時確定(區別於執行上下文環境,當然this也是上下文環境裏的成分)
// 全局作用域
let x = 100;
function bar() {
console.log(x);
}
// fn作用域
function fn() {
let x = 50; // bar作用域
bar();
}
fn(); // 100
作用域只是一個"地盤",其中沒有變量。變量時哦通過作用域對應的執行上下文環境中的變量對象來實現的。所以作用域是靜態觀念的,而執行上下文環境是動態上的,兩者並不一樣。
變量對象
執行上下文抽象成爲了一個對象,擁有個屬性,分別是變量對象,作用域鏈以及this指向。
- 變量對象裏面所擁有的東西
- Arguments對象
- 確定形式參數,檢查上下文中函數聲明
- 找到每一個函數聲明
- 就在 variableObject 下用函數名創建一個屬性,屬性值就指向該函數在內存中的地址的一個引用。
- 確定當前上下文中的局部變量,如果遇到和函數名同名的變量,則會忽略該變量
- 通過例子來演示函數的這兩個階段以及變量對象是如何變化的。
const foo = function(i){
var a = "Hello";
var b = function privateB(){}; function c(){}
}
foo(10);
//首先在建立階段
fooExecutionContext = { variavleObject : {
arguments : {
0 : 10,length : 1
}, // 確定arguments對象
i : 10, // 確定形式參數
c : pointer to function c(), // 確定函數的引用
a : undefined, // 局部變量 初始值爲 undefined
b : undefined // 局部變量 初始值爲 undefined
},
scopeChain : {},
this : {}
}
14-2閉包(closure)
閉包:
- 一個函數中要嵌套一個內部函數,並且內部函數要訪問外部函數的變量
- 內部函數要被外部引用
例
function eat(){
var food = '雞翅';
return function(){
console.log(food);
} }
var look = eat();
look(); // 雞翅
look(); // 雞翅
//eat函數返回一個函數,並在這個內部函數中訪問food這個局部變量。調用eat函數並將結果look,這個look指向了eat函數中的內部函數,然後調用它,最終輸出food值
自由變量
- 自由變量可以理解成跨作用域的變量,比如子集作用域訪問作用域的變量。
例:
var school = function () {
var s1 = "小強"
var s2 = "小王";
var team = function (project) {
console.log(s1 + s2 + project);
}
return team;
} var team = school();
team("做電商項目"); // 小強小王做電商項目
team("做微信項目"); // 小強小王做微信項目
//變量s1和s2屬於school函數的局部變量,並被內部哈桑農戶team使用,同時該函數也被返回出去在外部,通過調用school得到了內部函數的引用,後面多次調用這個內部函數,仍然能夠訪問到s1和s2變量被team函數引用,即使創造它們的函數school執行完了,這些變量依然存在,這就是閉包
3:閉包的原理
因爲作用域對象包含該上下文中的 VO/AO對象,還有scope對象,所以內部函數可以訪問到函數的變量
4:閉包的優缺點
優點:
- 通過閉包可以讓外部環境訪問到函數內部的局部變量。
- 通過閉包可以讓局部變量持續保存下來,不隨着它的上下文環境一起銷燬。
缺點: - 局部變量本來應該在函數退出時被解除引用,但如果局部變量被封印在閉包形成的環境中,那麼這個局部變量就一直能生存下去,然而閉包的確會使一些數據無法被及時銷燬。
函數節流
- 目的:是爲了防止一個函數短時間內被頻繁的觸發。
- 原理:是將即將執行的函數用
setTimeout()
延遲一段時間執行。而函數節流的原理是讓連續的函數執行,變爲固定時間段間斷的執行
1:使用時間戳
- 觸發事件時,取出當前的時間戳,然後減去之前的時間戳(最一開始值設爲0),如果大於設置的時間週期,就執行函數,然後更新時間戳爲當前的時間戳,如果小於,就不執行。
例:
function throttle(func, wait) {
let context, args;
let previous = 0;
return function () {
let now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
2:使用定時器
- 觸發事件時,設置一個定時器,再觸發事件的時候,如果定時器存在,就不執行,知道定時器執行,然後執行函數,清空定時器,這樣就可以設置一個定時器。
例:
function throttle(func, wait) {
let timeout, args, context;
let previous = 0;
return function () {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args)
}, wait);
}
}
}
14-3(分時函數)
timeChunk()函數接受3個參數。第一個參數是創建節點時需要用到的數據,第二個參數是封裝了創建節點邏輯的函數,第三個參數表示每一批創建的節點數量
<body>
<script>
const timeChunk = function (ary, fn, count) {
let obj, t;
let len = ary.length;
const start = function () {
for (let i = 0; i < Math.min(count || 1, ary.length); i++) { let obj = ary.shift();
fn(obj);
}
};
return function () {
t = setInterval(function () {
//如果全部節點都已經被創建好
if (ary.length === 0) {
return clearInterval(t);
}
start();
}, 200); //分批執行的時間間隔,也可以用參數的形式傳入
};
};
//最後進行一些測試,假設我們有1000個好友的數據
// 然後利用timeChunk函數,每一批直往頁面中創建8個節點
const ary = [];
for (let i = 1; i <= 1000; i++) {
ary.push(i);
}
const renderFriendList = timeChunk(ary, function (n) {
const div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
}, 8);
renderFriendList();
</script>
</body>
可以看到1000條數據是分批創建添加的
##14-4(惰性函數)
在Web開發中,因爲瀏覽器之間實現的差異,一些嗅探工作總是不可避免的,比如需要在各個瀏覽器中能夠通用的事件綁定函數addEvent()
解決方式:
- 惰性載入函數方法
此時add Event()
依然被聲明爲一個普通函數,在函數裏依然有一些分支判斷。但是在第一次進入條件分支之後,在函數內部會重寫這個函數,重寫之後的函數就是我們所期望的add Event()
函數,在下一次進入時它不再存在條件分支
<body>
<div id="div1">點我綁定事件</div>
<script>
const addEvent = function (elem, type, handler) {
if (window.addEventListener) {
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false);
}
}
if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent('on' + type, handler);
}
}
addEvent(elem, type, handler);
// 第一次綁定事件的時候需要手動調用以下
};
const div = document.getElementById('div1');
addEvent(div, 'click', function () {
alert(1);
});
addEvent(div, 'click', function () {
alert(2);
});
</script>
</body>