js面向對象以及編程範式 js高級部分

面向對象


面向對象編程(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開發中高併發的需求越來越多,多核並行的程序涉及被推倒前線,而傳統的命令式編程天生的缺陷使得並行編程模型變得非常複雜,使程序員不堪其重。函數式編程在解決兵法程序上面卻有着天然的優勢,代碼簡潔優雅,但是缺點就是學習門檻高。

面向對象

一、面向對象基本特徵

  1. 封裝:也就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
  2. 繼承:通過繼承創建的新類稱爲“子類”或“派生類”。繼承的過程,就是從一般到特殊的過程。
  3. 多態:對象的多功能,多方法,一個方法多種表現形式。
  4. Javascript是一種基於對象(object-based)的語言。但是,它又不是一種真正的面向對象編程(OOP)語言,因爲它的語法中沒有class(類)—–es6以前是這樣的。所以es5只有使用函數模擬的面向對象。

二、對象實例化方式

  1. 原始模式:這樣的寫法有兩個缺點,一是如果多生成幾個(100個!)實例,寫起來就非常麻煩;二是實例與原型之間,沒有任何辦法,可以看出沒有什麼聯繫。
var Car = {
    color: 'red',//車的顏色
    wheel: 4,//車輪數量
}
var Car2 = {
    color: 'blue',
    wheel: 4,
}
alert(Car.color);//red
  1. 原始模式的改進:通過寫一個函數,解決代碼重複的問題。
function createCar(color,wheel) {
    return {
        color:color,
        wheel:wheel
    }
}
//然後生成實例對象,就等於是在調用函數:
var cat1 = createCar("紅色","4");
var cat2 = createCar("藍色","4");

alert(cat1.color);//紅色
  1. 工廠模式
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);//紅色
  1. 構造函數模式:爲了解決從原型對象生成實例的問題,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);//紅色

三、構造函數注意事項

  1. 此時CreateCar稱之爲構造函數,也可以稱之類,構造函數就是類 。
  2. cat1,cat2均爲CreateCar的實例對象。
  3. CreateCar構造函數中this指向CreateCar實例對象即 new CreateCar( )出來的對象。
  4. 必須帶new 。
  5. 構造函數首字母大寫,這是規範,官方都遵循這一個規範,如Number() Array()。
  6. contructor:這時cat1和cat2會自動含有一個constructor屬性,指向它們的構造函數,即CreateCar。
alert(cat1.constructor == CreateCar); //true
alert(cat2.constructor == CreateCar); //true
  1. 每定義一個函數,這個函數就有一個 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,//是否可以再次修改修飾符
});

面向對象總結

  1. 所謂編程範式,指的是計算機編程的基本風格或典範模式。大致可以分爲命令式編程和聲明式編程。
  2. 面向對象的程序設計是站在哲學的角度上,將人類思維融入到了程序裏面的一種編程範式。
  3. 描述對象的時候可以通過兩個方法來進行描述。分別是對象的外觀和功能。在程序中與之對應的就是屬性和方法。
  4. JavaScript中的對象都是基於原型的,從一個原型對象中可以克隆出一個新的對象。
  5. 在JavaScript中每個對象都有一個原型對象。可以通過__proto__屬性來找到一個對象的原型對象。
  6. 在其他語言中,對象從類中產生。而JavaScript中,通過構造函數來模擬其他語言中的類。
  7. 類與對象的關係可以總結爲:類是對對象的一種概括,而對象是類的一種具體實現。
  8. 面向對象的三大特徵:封裝、繼承、多態
  9. 封裝是指對象的屬性或者方法隱藏於內部,不暴露給外部。
  10. 繼承是指一個子類繼承一個父類。在繼承了父類之後,子類就擁有了父類的所有屬性和方法。
  11. 多態是指不同的對象可以擁有相同的方法,不過是以不同的方式來實現它。
  12. this的指向是根據使用的地方不同分爲好幾種情況,但是我們可以通過一些方式來修改this的指向。

執行上下文

  • 先進後出,後進先出。
函數的執行上下文的生命週期

執行上下文的生命週期分爲兩個階段:

  1. 創建階段(進入執行上下文):函數被調用時,進入函數環境,爲其創建一個函數上下文
  2. 執行階段(代碼執行):執行函數中代碼時,此時執行上下文進入執行狀態。
創建階段的操作
  1. 創建變量對象
  • 函數環境會初始化創建 Arguments 對象,形式參數(並賦值)。
  • 普通函數聲明(並賦值)
  • 局部變量聲明,函數表達式聲明(未賦值)
  1. 初始化作用域鏈
  2. 確定this指向(this由調用者確定)
  3. 確定作用域(詞法環境決定,哪裏聲明定義,就在哪裏確定)
執行階段的操作
  1. 變量對象賦值
  • 變量賦值
  • 函數表達式賦值
  1. 調用函數
  2. 按順序執行其他代碼
執行上下文與作用域區別

作用域和執行上下文不是同一個概念。

執行全局代碼時,會產生一個執行上下文環境,每次調用函數都會執行上下文環境。當函數調用完時,這個上下文環境以及其中的數據都會被消除(除了閉包),處於活動狀態的執行上下文環境只有一個

而作用域在函數定義時就已經確定了,不是在函數調用時確定(區別於執行上下文環境,當然this也是上下文環境裏的成分)

// 全局作用域
let x = 100;
function bar() {    
  console.log(x);
} 
// fn作用域
function fn() {  
  let x = 50;    // bar作用域
  bar(); 
}
fn(); // 100

作用域只是一個"地盤",其中沒有變量。變量時哦通過作用域對應的執行上下文環境中的變量對象來實現的。所以作用域是靜態觀念的,而執行上下文環境是動態上的,兩者並不一樣。

變量對象

執行上下文抽象成爲了一個對象,擁有個屬性,分別是變量對象,作用域鏈以及this指向

  • 變量對象裏面所擁有的東西
  1. Arguments對象
  2. 確定形式參數,檢查上下文中函數聲明
  3. 找到每一個函數聲明
  • 就在 variableObject 下用函數名創建一個屬性,屬性值就指向該函數在內存中的地址的一個引用。
  1. 確定當前上下文中的局部變量,如果遇到和函數名同名的變量,則會忽略該變量
  • 通過例子來演示函數的這兩個階段以及變量對象是如何變化的。
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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章