摘要:通過上面的圖我想應該一目了然了,基本數據類型都是存在棧內存中的,復雜對象則是存在堆內存中,棧內存變量保存的是其內存地址。等同于如果使用則規則相對簡單值為基本數據類型數值轉為相應的字符串。
數據類型是基礎中的基礎,大家天天遇到,我們這次來討論深一點,將我們認為理所當然的事情背后的原理發掘;繼承也是前端基礎一個大考點,看看繼承的原理與使用場景。
本文討論以下幾個點:
JavaScript數據類型
不同數據類型對應的數據結構
數據類型轉換
數組與對象的api表
new關鍵字背后干了些什么
原型與原型屬性與構造函數區別于聯系
實例化,混入,繼承,多態是什么意思
數據類型最新的 ECMAScript 標準定義了 7 種數據類型:
6 種原始類型:
Boolean
Null
Undefined
Number
String
Symbol (ECMAScript 6 新定義)
和 Object
這個分類我們應該是相當熟悉了,當時這是按照什么標準分類的。
數據類型對應的數據結構事實上上面的分類標準是按照不同數據在計算機內存中的結構分類的。我們都知道JavaScript中的變量運行的時候是存在內存中的,如果接觸過java的人應該知道,內存中也分為棧內存和堆內存。
棧(stack)基本類型Undefined、Null、Boolean、Number 和String。這些類型在內存中分別占有固定大小的空間,他們的值保存在棧內存,他們的值保存在棧內存,我們通過按值來訪問的。
var a = "1"; var b = "1"; a === b;
上述代碼執行時候,可以理解為:
聲明變量a,b,為a,b分配一個棧內存空間。(變量提升)
要賦值a,將a的字面量"1"作為值存儲到a在棧內存的值中。
要賦值b,同樣將"1"作為棧內存的值存儲。
這種簡單數據類型,都是在棧內存中保存其值。
JavaScript中的原始值(基本數據類型)都可預知其最大最小內存大小,所以創建的時候直接分配對應的內存空間。
堆(heap)復雜的數據類型,如object,array,function等,無法提前預知其要占用多少內存空間,所以這個數據類型被放入了堆內存中,同時在棧內存中保存其堆內存的地址,訪問這些變量的時候,在棧內存中獲取到其內存地址,然后訪問到該對象,這種方式叫按引用訪問。
var a = "hello world"; var b = 123; var c = null; var d = undefined; var e = {}; var f = function(){console.log(1);}; var g = [1,2,a];
其在內存中的簡易模型如下:
上面這個圖并不是完全準確的,這里只是簡單形容一下不同數據類型變量的存儲關系,偏底層的知識真的需要多帶帶開一篇來講了。
通過上面的圖我想應該一目了然了,基本數據類型都是存在棧內存中的,復雜對象則是存在堆內存中,棧內存變量保存的是其內存地址。這也應該想到了我們經常遇到的問題:對象之間賦值,賦值的是真正的內存地址;對象相互比較===,比較的是內存地址。
變量賦值JavaScript 引用指向的是值。如果一個值有 10 個引用,這些引用指向的都是同一個值,它們相互之間沒有引用 / 指向關系。
JavaScript 對值和引用的賦值 / 傳遞在語法上沒有區別,完全根據值的類型來決定:
簡單數據類型總是通過值復制的方式來賦值 / 傳遞。
復雜數據類型則總是通過引用復制的方式來賦值 / 傳遞。
包裝類和類型轉換 內置對象JavaScript內置了一些對象,這些對象可以在全局任意地方調用,并且有各自的屬性和方法。MDN上羅列了全部,這里只挑一部分對象說明:
Object
Array
Function
String
Number
Boolean
Math
Date
RegExp
ok通過上面的幾個內置對象就會發現一些問題:一些基本數據類型(String,Number,Boolean)有對應的內置對象,但是其他的一些(Null, Undefined)就沒有,復雜數據類型則都有,這是為什么。
包裝類var a = "hello world"; a[1]; // "e" a.length; // 11 a.toString(); // hello world a.valueOf(); // hello world a.split(" "); // ["hello", "world"]
有沒有想過,變量a命名是個基本類型,不是對象,為什么會有這么多屬性和方法。因為這些內置的屬性和方法都在內置對象String上。
事實上當你調用這些基本數據類型上屬性和方法時候,引擎會自動尋找其是否有對應的包裝類,有的話生成一個包裝類的實例供你使用(使用之后銷毀),否則報錯。
var a = "hello world"; a.customAttribute // undefined String.prototype.customAttribute = "custom"; var b = "hello world"; b.customAttribute // custom
我們現在想要訪問屬性customAttribute,這個屬性沒有在內置對象上,所以獲取到的值是undefined;我們向內置對象的原型鏈上添加該屬性,之后所有的string上都可以獲取到該值。
類型轉換JavaScript中的類型轉換也是個大坑,不少面試都會問到。JavaScript 是一種動態類型語言,變量沒有類型限制,可以隨時賦予任意值。
顯示轉換
直接調用對應的包裝類進行轉換。具體可分成三種情況:
// 數值:轉換后還是原來的值 Number(324) // 324 // 字符串:如果可以被解析為數值,則轉換為相應的數值 Number("324") // 324 // 字符串:如果不可以被解析為數值,返回 NaN Number("324abc") // NaN // 空字符串轉為0 Number("") // 0 // 布爾值:true 轉成 1,false 轉成 0 Number(true) // 1 Number(false) // 0 // undefined:轉成 NaN Number(undefined) // NaN // null:轉成0 Number(null) // 0
使用Number包裝類來進行類型轉換,隱藏的邏輯:
調用對象自身的valueOf方法。如果返回原始類型的值,則直接對該值使用Number函數,不再進行后續步驟。
如果valueOf方法返回的還是對象,則改為調用對象自身的toString方法。如果toString方法返回原始類型的值,則對該值使用Number函數,不再進行后續步驟。
如果toString方法返回的是對象,就報錯。
var obj = {x: 1}; Number(obj) // NaN // 等同于 if (typeof obj.valueOf() === "object") { Number(obj.toString()); } else { Number(obj.valueOf()); } var obj1 = { valueOf: function () { return {}; }, toString: function () { return {}; } }; Number(obj1) // TypeError: Cannot convert object to primitive value Number({ valueOf: function () { return 2; } }) // 2 Number({ toString: function () { return 3; } }) // 3 Number({ valueOf: function () { return 2; }, toString: function () { return 3; } }) // 2
如果使用String則規則相對簡單:
值為基本數據類型
數值:轉為相應的字符串。
字符串:轉換后還是原來的值。
布爾值:true轉為字符串"true",false轉為字符串"false"。
undefined:轉為字符串"undefined"。
null:轉為字符串"null"。
值為對象
先調用對象自身的toString方法。如果返回原始類型的值,則對該值使用String函數,不再進行以下步驟。
如果toString方法返回的是對象,再調用原對象的valueOf方法。如果valueOf方法返回原始類型的值,則對該值使用String函數,不再進行以下步驟。
如果valueOf方法返回的是對象,就報錯。
Boolean規則更簡單:除了五個值(undefined,null,(+/-)0,NaN,‘’)的轉換結果為false,其他的值全部為true。
隱式轉換
隱式轉換也分三種情況:
轉布爾值
JavaScript 遇到預期為布爾值的地方(比如if語句的條件部分),就會將非布爾值的參數自動轉換為布值。系統內部會自動調用Boolean函數。
所以跟上面一樣,因此除了五個值(undefined,null,(+/-)0,NaN,‘’),其他都是自動轉為true。
轉字符串
JavaScript 遇到預期為字符串的地方,就會將非字符串的值自動轉為字符串。具體規則是,先將復合類型的值轉為原始類型的值,再將原始類型的值轉為字符串。
字符串的自動轉換,主要發生在字符串的加法運算時。當一個值為字符串,另一個值為非字符串,則后者轉為字符串。
"5" + 1 // "51" "5" + true // "5true" "5" + false // "5false" "5" + {} // "5[object Object]" "5" + [] // "5" "5" + function (){} // "5function (){}" "5" + undefined // "5undefined" "5" + null // "5null"
轉數值
JavaScript 遇到預期為數值的地方,就會將參數值自動轉換為數值。系統內部會自動調用Number函數。
除了加法運算符(+)有可能把運算子轉為字符串,其他運算符都會把運算子自動轉成數值。
"5" - "2" // 3 "5" * "2" // 10 true - 1 // 0 false - 1 // -1 "1" - 1 // 0 "5" * [] // 0 false / "5" // 0 "abc" - 1 // NaN null + 1 // 1 undefined + 1 // NaN
具體參考阮一峰老師:JavaScript類型轉換
數組和對象這三個復雜對象我們太熟悉不過了,每天都在打交道。但是實際上我們也并不是完全掌握。
數組(Array)數組方法很多,我們可以分類來整理記憶。
有哪些方法返回的是新數組
concat
slice
filter
map
forEach
遍歷數組方法有幾種,區別在于什么
常見的有:
map:返回新數組,數組的每一項都是測試函數的返回值。
forEach:不返回任何值,只是單純遍歷一遍數組。
every:遍歷數組所有元素,直到測試函數返回第一個false停止。
some:遍歷數組所有元素,直到測試函數返回第一個true停止。
for循環:寫起來最麻煩,但是性能最好。
filter方法傳入函數的參數有幾個,都是什么含義
不只是filter方法,類似這種第一個參數為callback的方法如:some,every,forEach,map,find,findIndex的方法callback參數都一樣:currentValue,Index,array。
github上參照了MDN整理了一份完整的文檔,用于自己的查缺補漏。
創建對象的方法有幾種
字面量方式:
var person={ name:"SF", age:25 say:function(){ alert(this.name+"今年"+this.age); } }; person.say();
利用Object對象創建實例
var my = new Object(); my.name = "SF"; //JavaScript的發明者 my.age = 25; my.say = function() { alert("我是"+this.name+"今年"+my.age); } my.say(); var obj = Object.create(null); obj.name = "SF";
構造函數
function Person(name,age) { this.name = name; this.age = age; this.say = function() { alert("我叫" + this.name + ",今年" + this.age + "歲); } } var my = new Person("SF",25); //實例化、創建對象 my.say(); //調用say()方法
原型模式
function Person() { } Person.prototype.name = "aus"; Person.prototype.job = "fe" Person.prototype.sayName = function() { console.log(this.name) } var person1 = new Person();
組合構造函數和原型
function Person( name, age, job ) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"]; } Person.prototype = { constructor: Person, sayName: function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
對象的擴展密封和凍結有什么區別
擴展特性
Object.isExtensible 方法
Object.preventExtensions 方法
密封特性
Object.isSealed 方法
Object.seal 方法
凍結特性
Object.isFrozen 方法
Object.freeze 方法
淺凍結 與 深凍結
簡單說就是對象有可擴展性(可以隨意添加屬性),限制對象的可擴展性(Object.preventExtensions)之后,對象不可添加新屬性(但是現有屬性可以修改和刪除)。
密封對象(seal)指的是對象的屬性不可增加或者刪除,并且屬性配置不可修改(屬性值可修改)。
凍結對象(freeze)則更加嚴格,不可增加或者刪除屬性,并且屬性完全不可修改。
這里不做過多介紹,詳細可以看這里。
怎樣快速實現淺拷貝以及深拷貝
Object.assign是常見的淺拷貝方法,怎樣自己實現。
// 利用原生api function shallowClone(obj) { return Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) ); } // 屬性淺拷貝 function shallowCopy(copyObj) { var obj = {}; for ( var i in copyObj) { obj[i] = copyObj[i]; } return obj; }
深拷貝之前整理過:github。
對象的方法參照MDN整理了一份,github。
原型鏈這節算是給繼承鋪墊基礎知識了,js里最出名的原型和原型鏈,面試必考,日常開發也特別常見。
prototypeprototype中文譯為"原型",大部分Object和Function都有prototype。個人覺得原型是一個特殊的普通對象,對象里面的屬性和方法都用于指定的用途:共享。我們可以按照自己的意愿去修改原型,并且重新被共享。
當創建函數的時候,每一個函數都會自動有一個prototype屬性,這個屬性的值是空對象(空對象不是空)。
一旦你把這個函數當成構造函數調用(通過new調用)JS會創建構造函數的實例,實例是不具有原型的。
function A (){}; A.prototype // {} var a = new A(); a.prototype // undefinedproto
中文翻譯過來叫"原型屬性",這是一個隱式屬性,不可被枚舉,但是他的用途至關重要。每個對象創建的時候,都會有一個隱式的屬性__proto__,該屬性的值是其對應的原型(其實就是說明 該對象的來源)。
function A (){}; A.__proto__ === Function.prototype; // true var b = {}; b.__proto__ === Object.prototype; // true var c = []; c.__proto__ === Array.prototype; // true
可以確定的是,__proto__指向的是其構造函數的原型。
contructor構造函數實例都擁有指向其構造函數的constructor屬性。constructor屬性的值是一個函數對象 為了將實例的構造器的原型對象暴露出來。
function A(){}; A.constructor === Function // true var a = new A(); a.construtor === A // true var obj = {}; obj.constructor === Object // true
可以確定的是,constructor屬性指向其構造函數。
關系上面三者的關系可以用下圖表示:
這里就不得不提一句:使用new關鍵字實例化對象,內在過程到底發生了什么。
我們可以理解為將new關鍵字實例化對象拆成兩步:
function A(){}; function create (base) { var obj = {}; obj.__proto__ = base.prototype; base.call(obj); return obj; } var a = create(A); a instanceof A // true原型鏈
上面三個角色到期了之后,就到了另一個重點:原型鏈。
var a = Object.create(null); a.a = 1; var b = Object.create(a); b.b = 2; var c = Object.create(b); c.c = 3; c.a // 1 c.b // 2 c.c // 3 a.d = 4; c.d; c.a = 0; c.a; // 0
上面這個例子用到了Object.create函數創建了一個原型為空的對象a。可以看到c并沒有a,b屬性,但是卻可以讀出該值來,這就是原型鏈。
當訪問一個對象的屬性(方法)的時候,如果對象自身沒有該屬性(方法),就會去該對象的__proto__上尋找,如果__proto__上也沒有,就去__proto__.__proto__上尋找,以此類推,直到找到一個值返回;若沒有則返回undefined。這種按照對象原型屬性尋找形成一個類似鏈狀的結構,叫做原型鏈。
畫個圖表示:
上圖中的__proto__紅線可以理解為原型鏈。
這里要注意的是,對象的原型屬性,保存的是對象的內存地址引用,需要讀取原型屬性的時候會找到該對象當時的狀態,所以更改原型鏈上原型屬性對象,會對該條原型鏈上的其他對象造成影響。
繼承ok經過這么多鋪墊終于來到了繼承,繼承是面向對象里面最重要的概念之一。我們先來把相關概念介紹,再來看動手實現。
不管是實例,混入或者繼承,他們的誕生都是為了解決同一個問題:代碼復用。只不過實現方式不同。
實例這個是我們日常開發中最常用的一種。
var date = new Date(); var instanceLightBox = new LightBox();
實例化一個對象可以理解為調用類的構造函數,返回一個擁有類所有屬性和方法的對象。
這樣說可能也不準確,我們以var a = new A();為例,實例化一個對象有幾個特點:
a是一個object;
a的構造函數是A;
A構造函數中的非私有屬性會被a獲取到;
A的原型是a的原型屬性;
function A () { this.a = 1; }; A.prototype.getA = function(){ return this.a; } var a = new A(); a.a; // 1 a.getA(); // 1
事實上我們在上面已經講解了調用new關鍵字發生了什么,這里原理不多講。為什么要用實例化類:我們可以吧構造函數當做一個工廠,工廠產出了定制化模板(構造函數)和標準模板(構造函數的原型)的產品;我們可以通過多次實例化一個類,產出多個一樣的產品,從而實現了代碼復用。
混入(mixin)混入更像是一個加工廠,對已有的對象進行添加新屬性的操作。
function A (){ this.a = 1; }; // 一個非常簡單的mixin例子 function mixin(sourceObj, targetObj){ for (var key in sourceObj) { // 只會在不存在的情況下復制 if (!(key in targetObj)) { targetObj[key] = sourceObj[key]; } } } var a = new A(); var b = {b:2}; mixin(b, a); a.b; // 2
這個例子可以看到,targetObj混入了sourceObj的特有屬性,如果屬性是方法或者對象的話,targetObj保存的知識對象的引用,而不是自己獨有的屬性,這樣sourceObject更改targetObj也會跟著更改。
繼承(extend)繼承里面有兩個角色,父類和子類。繼承理解為得到父類所有的屬性,并且可以重寫這些屬性。同樣是獲得一個function全部的屬性和方法,我認為實例和繼承的最大區別在于實例是構造函數實例對象,繼承是類繼承類,數據類型有明顯區別。
我們先來看看ES6中的繼承:
class Parent { constructor (props) { const {name, phone} = props; this.name = name; this.phone = phone; } getInfo(){ return this.name + ":" + this.phone; } } class Child extends Parent { constructor(props){ super(props); const {gender} = props; this.gender = gender; } getNewInfo(){ return this.name + ":" + this.gender + ":" + this.phone; } } var childIns = new Child({ name: "aus", gender: "male", phone: "1888888888" });
先不討論繼承是如何實現的,先來看看繼承的結果。ES6中的繼承,Child類拿到了Parent類的構造器里的非屬性和原型上的所有屬性,并且可以擴展自己的私有屬性和原型屬性。但是父類和子類仍然公用父類的原型。
繼承有三個特點:
子類擁有父類非私有的屬性和方法。
子類可以擁有自己屬性和方法,即子類可以對父類進行擴展。
子類可以用自己的方式實現父類的方法。
多態這里多態不詳細介紹,我們來了解概念與實例。
多態:同一操作作用于不同的對象,可以有不同的解釋,產生不同的執行結果。
舉個例子,父類原型上有個方法a,子類原型上有個同名方法a,這樣在子類實例上調用a方法必然是子類定義的a,但是我如果想用父類上的a怎么辦。
class Parent { constructor (props) { const {name, phone} = props; this.name = name; this.phone = phone; } getInfo(){ return this.name + ":" + this.phone; } } class Child extends Parent { constructor(props){ super(props); const {gender} = props; this.gender = gender; } getInfo(from){ // 全完自定義 if("child" === from){ return this.getNewInfo(); } else { return super.getInfo(); } } getNewInfo(){ return this.name + ":" + this.gender + ":" + this.phone; } } var childIns = new Child({ name: "aus", gender: "male", phone: "1888888888" });
多態是一個非常廣泛的話題,我們現在所說的“相對”只是多態的一個方面:任何方法都可以引用繼承層次中高層的方法(無論高層的方法名和當前方法名是否相同)。之所以說“相對”是因為我們并不會定義想要訪問的絕對繼承層次(或者說類),而是使用相對引用“查找上一層”。繼承實現
一道非常常見的面試題,有多種方法,分成兩個思路,篇幅有限,不過多介紹,詳細的文檔在github上,或者自行google。
參考《JavaScript權威指南》
《JavaScript高級程序設計》
《你所不知道的JavaScript》
JavaScript變量——棧內存or堆內存
內存管理
數據類型轉換
面向對象編程三大特性------封裝、繼承、多態
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94644.html
摘要:可實現單例模式代碼塊初始化靜態變量,只被執行一次內部類不能與外部類重名,只能訪問外部類靜態數據包括私有多分支選擇整型或字符類型變量或整數表達式開始支持。 前言 大學期間接觸 Java 的時間也不短了,不論學習還是實習,都讓我發覺基礎的重要性?;ヂ摼W發展太快了,各種框架各種技術更新迭代的速度非常快,可能你剛好掌握了一門技術的應用,它卻已經走在淘汰的邊緣了。 而學習新技術總要付出一定的時間...
摘要:在這些類當中有其自身的實現一般都是用來比較對象的成員變量值是否相同,而不再是比較類在堆內存中的存放地址了。舉例說明結果說明中有其自身的實現,屬于內容比較。 在研究hashcode的用法,發現自己對equals的理解還不夠深,廢話少說,繼續專研,記錄如下: 要想搞清楚equals和==的區別,就先要搞清楚JAVA的數據類型: showImg(https://segmentfault.co...
摘要:面向對象與面向過程的區別要知道,二者并不是非此即彼,而是相輔相成的。而面向過程,則在微觀上對對象內部進行具體的實現。面向對象的三大特性說到面向對象,就不得不說其三大特性封裝繼承和多態。封裝封裝是面向對象最基礎的特性。 作者:伯特出處:github.com/ruicbAndroid/LoulanPlan聲明:本文出自伯特的《LoulanPlan》,轉載務必注明作者及出處。 剛學習 Jav...
摘要:作用域分類作用域共有兩種主要的工作模型。換句話說,作用域鏈是基于調用棧的,而不是代碼中的作用域嵌套。詞法作用域詞法作用域中,又可分為全局作用域,函數作用域和塊級作用域。 一篇鞏固基礎的文章,也可能是一系列的文章,梳理知識的遺漏點,同時也探究很多理所當然的事情背后的原理。 為什么探究基礎?因為你不去面試你就不知道基礎有多重要,或者是說當你的工作經歷沒有亮點的時候,基礎就是檢驗你好壞的一項...
閱讀 1115·2021-11-16 11:42
閱讀 2895·2021-10-12 10:18
閱讀 2854·2021-09-24 09:48
閱讀 3457·2019-08-30 15:56
閱讀 1523·2019-08-30 14:17
閱讀 3036·2019-08-29 12:14
閱讀 902·2019-08-27 10:51
閱讀 2020·2019-08-26 13:28