JavaScript對象、函數和類


JavaScript是應用事件編程

javascript是通過單線程來執行,當有事件發生,這個線程不一定有時間,需要一個機制讓產生新事件等一等。這個機制就是Eventloop,從代碼的角度看,所有的邏輯都是通過七七八八的“異步回調”來完成的;而從程序員思維方式的角度看,以往基於線程的編程,變成了事件驅動的編程。

  對於邏輯的觸發,基於線程編程需要不斷地由監視線程去查詢被監視線程的某一個狀態,如果狀態滿足某個條件,則觸發相應的邏輯;而事件驅動則通過事件處理器,在事件發生時執行掛載的回調邏輯.(pull與push)

   基於線程的方式可以阻塞線程,等待時間或某個條件滿足后再繼續執行;而事件驅動則相反,發送一條消息后無阻塞等待回調的發生。阻塞線程的方式對資源的消耗往往更加顯著,因為無論是否執行線程都被占用,但從人直觀理解的角度來說,代碼更直白,更符合人對順序執行的理解;

而事件驅動的方式則相反,資源消耗上更優,但是代碼的執行順序,以及由此產生的系統狀態判斷變得難以預知。

 

一、Javascript對象

JavaScript本身就是面向對象的,JavaScript標准對基於對象的定義:語言和宿主的基礎設施由對象來提供,並且JavaScript程序即是一系列互相通訊的對象集合。

認識對象的起源:人類思維模式:我們總是先認識到某一個蘋果能吃(這里的某一個蘋果就是一個對象),繼而認識到所有的蘋果都可以吃(這里的所有蘋果,就是一個),再到后來我們才能意識到三個蘋果和三個梨之間的聯系,進而產生數字“3”()的概念。 

參考Grandy Booch《面向對象分析與設計》)。總結來看,對象有如下幾個特點。

  • 對象具有唯一標識性:即使完全相同的兩個對象,也並非同一個對象。
  • 對象有狀態:對象具有狀態,同一對象可能處於不同狀態之下。
  • 對象具有行為:即對象的狀態,可能因為它的行為產生變遷。

在 JavaScript中,將狀態和行為統一抽象為“屬性”。在實現了對象基本特征的基礎上, JavaScript中對象獨有的特色是:對象具有高度的動態性,這是因為JavaScript賦予了使用者在運行時為對象添改狀態和行為的能力。

對JavaScript來說,屬性並非只是簡單的名稱和值,JavaScript用一組特征(attribute)來描述屬性(property)。

1)數據屬性

  • value:就是屬性的值。
  • writable:決定屬性能否被賦值。
  • enumerable:決定for in能否枚舉該屬性。
  • configurable:決定該屬性能否被刪除或者改變特征值。

2)訪問器(getter/setter)屬性,它也有四個特征。

  • getter:函數或undefined,在取屬性值時被調用。
  • setter:函數或undefined,在設置屬性值時被調用。
  • enumerable:決定for in能否枚舉該屬性。
  • configurable:決定該屬性能否被刪除或者改變特征值。
  var o = { a: 1 };
    Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
    //a和b都是數據屬性,但特征值變化了
    Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
    Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
    o.b = 3;
    console.log(o.b); // 2

2、 Javascript采用了“原型”編程,也是面向對象編程的一種方式,沒有 class 化的,直接使用對象。

基於類的語言提倡使用一個關注分類和類之間關系的開發模型。與此相對,原型編程看起來提倡程序員關注一系列對象實例的行為,而之后才關心如何將這些對象划分到最近的使用方式相似的原型對象,而不是分成類。

每個對象都有一個 __proto__ 的屬性,這個就是“原型”。注意區分__proto__和prototype

注意:(1)__proto__ 主要是安放在一個實際的對象中,用它來產生一個鏈接,一個原型鏈連,用於尋找方法名或屬性,等等.

           (2)prototype 是用 new 來創建一個對象時構造 __proto__ 用的。它是一個只屬於 function 的屬性

__proto__ 是所有對象用於鏈接原型的一個指針,而 prototype 則是 Function 對象的屬性,其主要是用來當需要 new 一個對象時讓 __proto__ 指針所指向的地方。 對於超級對象 Function 而言, Function.__proto__ 就是 Function.prototype

3、JavaScript中的對象分類

宿主對象(host Objects):由JavaScript宿主環境提供的對象,它們的行為完全由宿主環境決定。前端最熟悉的無疑是瀏覽器環境中的宿主,全局對象window上的屬性

內置對象(Built-in Objects):由JavaScript語言提供的對象。固有對象(Intrinsic Objects );

原生對象(Native Objects);能夠通過語言本身的構造器創建的對象稱作原生對象,通過這些構造器,我們可以用new運算創建新的對象,幾乎所有這些構造器的能力都是無法用純JavaScript代碼實現的,它們也無法用class/extend語法來繼承。

 

 普通對象(Ordinary Objects):由{}語法、Object構造器或者class關鍵字定義類創建的對象,它能夠被原型繼承。

 二、函數

 

  JavaScript的函數是一等公民,如何理解這點呢。

函數可以不依附於任何類或對象等實體而獨立存在,它可以單獨作為參數、變量或返回值在程序中傳遞。

以往我們只能先指定宿主對象,再來調用函數;現在可以反過來,先指定函數,再來選擇宿主對象,完成調用。請注意,函數的調用必須要有宿主對象,如果你使用 null 或者 undefined 這樣不存在的對象,window 會取而代之,被指定為默認的宿主對象

 

 

通過獲取funtion屬性,看出prototype是function對象重要的屬性,存儲了Fuction的原型對象,prototype屬性的值類型是對象,prototype值下有的constructor是屬性,constructor 是一個比較特殊的屬性,它指向構造函數(類)本身。(閉環)

caller:返回調用者,全局作用域返回是null  

arguments:是已經廢棄的標准, 現在推薦的做法是使用函數內部可用的 arguments 對象來訪問函數的實參。

length:是形參的個數

 

function f() {}
console.log(Object.getOwnPropertyNames(f)); // [ 'length', 'name', 'arguments', 'caller', 'prototype' ]

  

用戶用function關鍵字創建的函數必定同時是函數和構造器。

function f(){
    return 1;
}
var v = f(); //把f作為函數調用
var o = new f(); //把f作為構造器調用

 new 運算接受一個構造器和一組調用參數,實際上做了幾件事:

  • 構造器的 prototype 屬性(注意與私有字段[[prototype]]的區分)為原型,創建新對象;
  • 將 this 和調用參數傳給構造器,執行;
  • 如果構造器返回的是對象,則返回,否則返回第一步創建的對象。

new 這樣的行為,試圖讓函數對象在語法上跟類變得相似,但是,它客觀上提供了兩種方式,一是在構造器中添加屬性 (在實例方法上),二是在構造器的 prototype 屬性上添加屬性

function Foo(y) {
  this.y = y;
}
 
// 修改 Foo 的 prototype,加入一個成員變量 x
Foo.prototype.x = 10;
 
// 修改 Foo 的 prototype,加入一個成員函數 calculate
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};
 
// 現在,我們用 Foo 這個原型來創建 b 和 c
var b = new Foo(20);
var c = new Foo(30);
 
// 調用原型中的方法,可以得到正確的值
b.calculate(30); // 60
c.calculate(40); // 80

內存中的布局是怎么樣的呢

 Foo.prototype 自動創建了一個屬性 constructor這是一個指向函數自己的一個 reference。這樣一來,

對於實例 b 或 c 來說,就能訪問到這個繼承的 constructor 了

 

console.log("Foo屬性", Object.getOwnPropertyNames(Foo)); //函數對象 [ 'length', 'name', 'arguments', 'caller', 'prototype' ]
console.log("Foo.prototype屬性", Object.getOwnPropertyDescriptor(Foo, "prototype")); //{ value: Foo { x: 10, calculate: [Function] },
//writable: true, enumerable: false, configurable: false }

let proto = Object.getOwnPropertyDescriptor(Foo, "prototype").value;
console.log("Foo.prototype.value", Object.getOwnPropertyNames(proto)); // ['constructor', 'x', 'calculate' ]
console.log(
    "Foo.prototype.constructor",
    Object.getOwnPropertyDescriptor(proto, "constructor") //{ value: [Function: Foo], writable: true, enumerable: false, configurable: true }
);

let con = Object.getOwnPropertyDescriptor(proto, "constructor").value;
console.log("Foo.prototype.constructor.value", Object.getOwnPropertyNames(con)); //函數對象 [ 'length', 'name', 'arguments', 'caller', 'prototype' ]
console.log("Foo.prototype.constructor.value的prototype屬性", Object.getOwnPropertyDescriptor(con, "prototype")); // { value: Foo { x: 10, calculate: [Function] },
// writable: true,
//     enumerable: false,
//     configurable: false
// }

 

不實例new,也就是普通的函數調用而已,所以若是函數本身沒有返回值,普通的函數調用沒有什么意義 
如果函數返回值為常規意義上的數值類型(Number、String、Boolean)時,new函數將會返回一個該函數的實例對象,
而如果函數返回一個引用類型(Object、Array、Function)時,則new函數與直接調用函數產生的結果相同。

三、 JavaScript類

類只能用new來創建,而不能使用()來做函數調用

ES6之后,函數分成三種,一是類,只可以做new運算,方法只可以調用(),一般函數同時可做new和調用運算  

我們對於類的概念完全是通過強大的函數特性來實現的,

理解this的行為

function Book(name) {
    console.log(this);
    this.name = name;
    return this;
}
new Book("Life"); // 打印 Book {}
Book("Life"); // 打印 Window { ... }
window.Book("Life") // 打印 Window { ... }

 

實際上,上述例子在使用 new 這個關鍵字的時候,JavaScript 引擎就幫我們做了這樣幾件事情。

第一件,創建一個 Book 的對象,我們把它叫做 x 吧。
第二件,綁定原型:x.proto = Book.prototype。
第三件,指定對象自己:this = x,並調用構造方法,相當於執行了 x.Book()。
第四件,對於構造器中的 return 語句,根據 typeof x === ‘object’ 的結果來決定它實際的返回:

  • 如果 return 語句返回基本數據類型(如 string、boolean 等),這種情況 typeof x 就不是“object”,那么 new 的時候構造器的返回會被強制指定為 x;
  • 如果 return 語句返回其它類型,即對象類型,這種情況 typeof x 就是“object”,那么 new 的時候會遵循構造器的實際 return 語句來返回。

 

如:Book1 的構造器返回一個基本數據類型的數值 1,new 返回的就是 Book1 的實例對象本身;而 Book2 的構造器返回一個非基本數值類型 [](數組),new 返回的就是這個數組了。

function Book1(name) {
    this.name = name;
    return 1;
}
console.log(new Book1("Life")); // 打印 Book1 {name: "Life"}
 
function Book2(name) {
    this.name = name;
    return [];
}
console.log(new Book2("Life")); // 打印 []

ES5 開始提供了嚴格模式(Strict Mode),可以讓代碼對一些可能造成不良后果的不嚴謹、有歧義的用法報錯。

在實際項目中,我們應當開啟嚴格模式,或是使用 TypeScript 這樣的 JavaScript 超集等等替代方案。寫 JavaScript 代碼的時候,心中要非常明確自己使用 function 的目的,是創建一個類,是創建某個對象的方法,還是創建一個普通的函數,並且在命名的時候,根據項目的約定給予清晰明確的名字,看到名字就立即可以知道它是什么,而不需要聯系上下文去推導,甚至猜測。

 

 

ES6中加入了新特性class,new跟function搭配的怪異行為終於可以退休了(雖然運行時沒有改變),在任何場景,我都推薦使用ES6的語法來定義類,而令function回歸原本的函數語義。

類的寫法實際上也是由原型運行時來承載的,邏輯上JavaScript認為每個類是有共同原型的一組對象,類中定義的方法和屬性則會被寫在原型對象之上

 一些激進的觀點認為,class關鍵字和箭頭運算符可以完全替代舊的function關鍵字,它更明確地區分了定義函數和定義類兩種意圖

   我們大致可以認為,它們[[construct]]的執行過程如下:

  • 以 Object.protoype 為原型創建一個新對象;
  • 以新對象為 this,執行函數的[[call]];
  • 如果[[call]]的返回值是對象,那么,返回這個對象,否則返回第一步創建的新對象。 

不使用new運算符,盡可能找到獲得對象的方法。

    • // 1. 利用字面量
      var a = [], b = {}, c = /abc/g
      // 2. 利用dom api
      var d = document.createElement('p')
      // 3. 利用JavaScript內置對象的api
      var e = Object.create(null)
      var f = Object.assign({k1:3, k2:8}, {k3: 9})
      var g = JSON.parse('{}')
      // 4.利用裝箱轉換
      var h = Object(undefined), i = Object(null), k = Object(1), l = Object('abc'), m = Object(true)

 

JavaScript 面向對象的知識:封裝、繼承以及多態。

在面向對象編程中,封裝(Encapsulation)說的是一種通過接口抽象將具體實現包裝並隱藏起來的方法。具體來說,封裝的機制包括兩大部分:

  • 限制對對象內部組件直接訪問的機制;
  • 將數據和方法綁定起來,對外提供方法,從而改變對象狀態的機制。

在靜態強類型的語言,比如java在類中通過 private 或 public 這樣的修飾符,能夠實現對對象屬性或方法不同級別的訪問權限控制。但是,在 JavaScript 中並沒有這樣的關鍵字這樣的 name 屬性,其實相當於公有屬性

function Book(name) {
    this.name = name;
}
console.log(new Book("Life").name);

並且利用閉包的特性,將 name 封裝在 Book 類的對象中,你無法通過任何其它方法訪問到私有屬性 name 的值。

閉包簡單說,就是引用了自由變量的函數。這里的關鍵是“自由變量”,其實這個自由變量,扮演的作用是為這個函數調用提供了一個“上下文”,

另外,引出一個重要概念,和閉包相對的,是一種稱為“純函數”(Pure Function)的東西,即函數不允許引用任何自由變量

function Book(name) {
    this.getName = () => {
        return name;
    };
    this.setName = (newName) => {
        name = newName;
    };
}
let book = new Book("Life");
book.setName("Time");
console.log(book.getName()); // Time
console.log(book.name); // 無法訪問私有屬性 name 的值

繼承

1、通過原型進行繼承

2、構造繼承

function Base1(name) {
    this.name = name;
}
function Base2(type) {
    this.type = type;
}
function Child(name, type) {
    Base1.call(this, name); // 讓 this 去調用 Base1,並傳入參數 name
    Base2.call(this, type);
}
 
var c = new Child("Life", "book");
console.log(c.name); // "Life"
console.log(c instanceof Base1); // false
console.log(c instanceof Child); // true

 

 

獲取全部JavaScript固有對象

我們從JavaScript標准中可以找到全部的JS對象定義。JS語言規定了全局對象的屬性。

三個值:
Infinity、NaN、undefined。

九個函數:

  • eval
  • isFinite
  • isNaN
  • parseFloat
  • parseInt
  • decodeURI
  • decodeURIComponent
  • encodeURI
  • encodeURIComponent

一些構造器:
Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeapSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError
URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。

四個用於當作命名空間的對象:

  • Atomics
  • JSON
  • Math
  • Reflect

我們使用廣度優先搜索,查找這些對象所有的屬性和Getter/Setter,就可以獲得JavaScript中所有的固有對象。

var set = new Set();
var vo=[ eval,
    isFinite,
    isNaN];
var fo=[eval,isFinite,isNaN, parseFloat,
    parseInt,
    decodeURI,
    decodeURIComponent,
    encodeURI,
    encodeURIComponent];
var co=[Array,
    Date,
    RegExp,
    Promise,
    Proxy,
    Map,
    WeakMap,
    Set,
    WeakSet,
    Function,
    Boolean,
    String,
    Number,
    Symbol,
    Object,
    Error,
    EvalError,
    RangeError,
    ReferenceError,
    SyntaxError,
    TypeError,
    URIError,
    ArrayBuffer,
    SharedArrayBuffer,
    DataView,
    Float32Array,
    Float64Array,
    Int8Array,
    Int16Array,
    Int32Array,
    Uint8Array,
    Uint16Array,
    Uint32Array,
    Uint8ClampedArray]
var no=[ Atomics,
    JSON,
    Math,
    Reflect]


var objects = [...vo,...fo,...co,...no];
objects.forEach(o => set.add(o));
 
for(var i = 0; i < objects.length; i++) {
    var o = objects[i]
    for(var p of Object.getOwnPropertyNames(o)) {
        
        var d = Object.getOwnPropertyDescriptor(o, p)        
        if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))           
         if(!set.has(d.value)){
            console.log("set>",d.value)
            set.add(d.value), objects.push(d.value);
         }             
        if( d.get )
            if(!set.has(d.get))
                set.add(d.get), objects.push(d.get);
        if( d.set )
            if(!set.has(d.set))
                set.add(d.set), objects.push(d.set);
    }
}

 深度搜索

Array:concat,copyWithin,fill,find,findIndex,lastIndexOf,pop,push,reverse,shift,unshift,slice,sort,splice,includes,indexOf,join,keys,entries,values,forEach,filter,flat,flatMap,map,every,some,reduce,reduceRight,toLocaleString,toString,isArray,from,of,
 Date:toString,toDateString,toTimeString,toISOString,toUTCString,toUTCString,getDate,setDate,getDay,getFullYear,setFullYear,getHours,setHours,getMilliseconds,setMilliseconds,getMinutes,setMinutes,getMonth,setMonth,getSeconds,setSeconds,getTime,setTime,getTimezoneOffset,getUTCDate,setUTCDate,getUTCDay,getUTCFullYear,setUTCFullYear,getUTCHours,setUTCHours,getUTCMilliseconds,setUTCMilliseconds,getUTCMinutes,setUTCMinutes,getUTCMonth,setUTCMonth,getUTCSeconds,setUTCSeconds,valueOf,getYear,setYear,toJSON,toLocaleString,toLocaleDateString,toLocaleTimeString,now,parse,UTC,
 RegExp: exec,get dotAll,get flags,get global,get ignoreCase,get multiline,get source,get sticky,get unicode,compile,toString,test,get input,set input,get $_,set $_,get lastMatch,set lastMatch,get $&,set $&,get lastParen,set lastParen,get $+,set $+,get leftContext,set leftContext,get $`,set $`,get rightContext,set rightContext,get $',set $',get $1,set $1,get $2,set $2,get $3,set $3,get $4,set $4,get $5,set $5,get $6,set $6,get $7,set $7,get $8,set $8,get $9,set $9,
 Promise:then,catch,finally,all,race,resolve,reject,allSettled,
 Proxy:revocable,
 Map:get,set,has,delete,clear,entries,forEach,keys,get size,values,
 WeakMap:delete,get,set,has,
 Set:has,add,delete,clear,entries,forEach,get size,values,values,
 WeakSet:delete,has,add,
 Function:,,,,,apply,bind,call,toString,
 Boolean:toString,valueOf,
 String:anchor,big,blink,bold,charAt,charCodeAt,codePointAt,concat,endsWith,fontcolor,fontsize,fixed,includes,indexOf,italics,lastIndexOf,link,localeCompare,match,matchAll,normalize,padEnd,padStart,repeat,replace,search,slice,small,split,strike,sub,substr,substring,sup,startsWith,toString,trim,trimStart,trimStart,trimEnd,trimEnd,toLocaleLowerCase,toLocaleUpperCase,toLowerCase,toUpperCase,valueOf,fromCharCode,fromCodePoint,raw,
 Number:toExponential,toFixed,toPrecision,toString,valueOf,toLocaleString,isFinite,isInteger,isNaN,isSafeInteger,parseFloat,parseInt,
 Symbol:toString,valueOf,get description,for,keyFor,
 Object:__defineGetter__,__defineSetter__,hasOwnProperty,__lookupGetter__,__lookupSetter__,isPrototypeOf,propertyIsEnumerable,toString,valueOf,get __proto__,set __proto__,toLocaleString,assign,getOwnPropertyDescriptor,getOwnPropertyDescriptors,getOwnPropertyNames,getOwnPropertySymbols,is,preventExtensions,seal,create,defineProperties,defineProperty,freeze,getPrototypeOf,setPrototypeOf,isExtensible,isFrozen,isSealed,keys,entries,values,fromEntries,
 Error:toString,captureStackTrace,
 EvalError:toString,
 RangeError:toString,
 ReferenceError:toString,
 SyntaxError:toString,
 TypeError:toString,
 URIError:toString,
 ArrayBuffer:get byteLength,slice,isView,
 SharedArrayBuffer:get byteLength,slice,
 DataView:get buffer,get byteLength,get byteOffset,getInt8,setInt8,getUint8,setUint8,getInt16,setInt16,getUint16,setUint16,getInt32,setInt32,getUint32,setUint32,getFloat32,setFloat32,getFloat64,setFloat64,getBigInt64,setBigInt64,getBigUint64,setBigUint64,

 undefined:defineProperty,deleteProperty,apply,construct,get,getOwnPropertyDescriptor,getPrototypeOf,has,isExtensible,ownKeys,preventExtensions,set,setPrototypeOf,

  JavaScript的函數是一等公民,如何理解這點呢。

函數可以不依附於任何類或對象等實體而獨立存在,它可以單獨作為參數、變量或返回值在程序中傳遞。

以往我們只能先指定宿主對象,再來調用函數;現在可以反過來,先指定函數,再來選擇宿主對象,完成調用。請注意,函數的調用必須要有宿主對象,如果你使用 null 或者 undefined 這樣不存在的對象,window 會取而代之,被指定為默認的宿主對象


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM