筆者最近在對原生JS的知識作系統梳理,由於我以爲JS做爲前端工程師的根本技術,學再多遍都不爲過。打算來作一個系列,一共分三次發,以一系列的問題爲驅動,固然也會有追問和擴展,內容系統且完整,對初中級選手會有很好的提高,高級選手也會獲得複習和鞏固。敬請你們關注!html
在 JS 中,存在着 7 種原始值,分別是:前端
引用數據類型: 對象Object(包含普通對象-Object,數組對象-Array,正則對象-RegExp,日期對象-Date,數學函數-Math,函數對象-Function)golang
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
複製代碼
結果:面試
p1:{name: 「fyq」, age: 26}
p2:{name: 「hzj」, age: 18}
複製代碼
緣由: 在函數傳參的時候傳遞的是對象在堆中的內存地址值,test函數中的實參person是p1對象的內存地址,經過調用person.age = 26確實改變了p1的值,但隨後person變成了另外一塊內存空間的地址,而且在最後將這另一分內存空間的地址返回,賦給了p2。chrome
結論: null不是對象。編程
解釋: 雖然 typeof null 會輸出 object,可是這只是 JS 存在的一個悠久 Bug。在 JS 的最第一版本中使用的是 32 位系統,爲了性能考慮使用低位存儲變量的類型信息,000 開頭表明是對象然而 null 表示爲全零,因此將它錯誤的判斷爲 object 。segmentfault
其實在這個語句運行的過程當中作了這樣幾件事情:api
var s = new Object('1');
s.toString();
s = null;
複製代碼
第一步: 建立Object類實例。注意爲何不是String ? 因爲Symbol和BigInt的出現,對它們調用new都會報錯,目前ES6規範也不建議用new來建立基本類型的包裝類。數組
第二步: 調用實例方法。瀏覽器
第三步: 執行完方法當即銷燬這個實例。
整個過程體現了基本包裝類型
的性質,而基本包裝類型偏偏屬於基本數據類型,包括Boolean, Number和String。
參考:《JavaScript高級程序設計(第三版)》P118
0.1和0.2在轉換成二進制後會無限循環,因爲標準位數的限制後面多餘的位數會被截掉,此時就已經出現了精度的損失,相加後因浮點數小數位的限制而截斷的二進制數字在轉換爲十進制就會變成0.30000000000000004。
BigInt是一種新的數據類型,用於當整數值大於Number數據類型支持的範圍時。這種數據類型容許咱們安全地對
大整數
執行算術操做,表示高分辨率的時間戳,使用大整數id,等等,而不須要使用庫。
在JS中,全部的數字都以雙精度64位浮點格式表示,那這會帶來什麼問題呢?
這致使JS中的Number沒法精確表示很是大的整數,它會將很是大的整數四捨五入,確切地說,JS中的Number類型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991((2^53-1)),任何超出此範圍的整數值均可能失去精度。
console.log(999999999999999); //=>10000000000000000
複製代碼
同時也會有必定的安全性問題:
9007199254740992 === 9007199254740993; // → true 竟然是true!
複製代碼
要建立BigInt,只須要在數字末尾追加n便可。
console.log( 9007199254740995n ); // → 9007199254740995n
console.log( 9007199254740995 ); // → 9007199254740996
複製代碼
另外一種建立BigInt的方法是用BigInt()構造函數、
BigInt("9007199254740995"); // → 9007199254740995n
複製代碼
簡單使用以下:
10n + 20n; // → 30n
10n - 20n; // → -10n
+10n; // → TypeError: Cannot convert a BigInt value to a number
-10n; // → -10n
10n * 20n; // → 200n
20n / 10n; // → 2n
23n % 10n; // → 3n
10n ** 3n; // → 1000n
const x = 10n;
++x; // → 11n
--x; // → 9n
console.log(typeof x); //"bigint"
複製代碼
BigInt不支持一元加號運算符, 這多是某些程序可能依賴於 + 始終生成 Number 的不變量,或者拋出異常。另外,更改 + 的行爲也會破壞 asm.js代碼。
由於隱式類型轉換可能丟失信息,因此不容許在bigint和 Number 之間進行混合操做。當混合使用大整數和浮點數時,結果值可能沒法由BigInt或Number精確表示。
10 + 10n; // → TypeError
複製代碼
Math.max(2n, 4n, 6n); // → TypeError
複製代碼
if(0n){//條件判斷爲false
}
if(3n){//條件爲true
}
複製代碼
元素都爲BigInt的數組能夠進行sort。
BigInt能夠正常地進行位運算,如|、&、<<、>>和^
caniuse的結果:
其實如今的兼容性並不怎麼好,只有chrome6七、firefox、Opera這些主流實現,要正式成爲規範,其實還有很長的路要走。
咱們期待BigInt的光明前途!
對於原始類型來講,除了 null 均可以調用typeof顯示正確的類型。
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
複製代碼
但對於引用數據類型,除了函數以外,都會顯示"object"。
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
複製代碼
所以採用typeof判斷對象數據類型是不合適的,採用instanceof會更好,instanceof的原理是基於原型鏈的查詢,只要處於原型鏈中,判斷永遠爲true
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str1 = 'hello world'
str1 instanceof String // false
var str2 = new String('hello world')
str2 instanceof String // true
複製代碼
能。好比下面這種方式:
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
複製代碼
若是你不知道Symbol,能夠看看MDN上關於hasInstance的解釋。
其實就是自定義instanceof行爲的一種方式,這裏將原有的instanceof方法重定義,換成了typeof,所以可以判斷基本數據類型。
核心: 原型鏈的向上查找。
function myInstanceof(left, right) {
//基本數據類型直接返回false
if(typeof left !== 'object' || left === null) return false;
//getProtypeOf是Object對象自帶的一個方法,可以拿到參數的原型對象
let proto = Object.getPrototypeOf(left);
while(true) {
//查找到盡頭,還沒找到
if(proto == null) return false;
//找到相同的原型對象
if(proto == right.prototype) return true;
proto = Object.getPrototypeof(proto);
}
}
複製代碼
測試:
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
複製代碼
Object在嚴格等於的基礎上修復了一些特殊狀況下的失誤,具體來講就是+0和-0,NaN和NaN。 源碼以下:
function is(x, y) {
if (x === y) {
//運行到1/x === 1/y的時候x和y都爲0,可是1/+0 = +Infinity, 1/-0 = -Infinity, 是不同的
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
//NaN===NaN是false,這是不對的,咱們在這裏作一個攔截,x !== x,那麼必定是 NaN, y 同理
//兩個都是NaN的時候返回true
return x !== x && y !== y;
}
複製代碼
解析:
== 中,左右兩邊都須要轉換爲數字而後進行比較。
[]轉換爲數字爲0。
![] 首先是轉換爲布爾值,因爲[]做爲一個引用類型轉換爲布爾值爲true,
所以![]爲false,進而在轉換成數字,變爲0。
0 == 0 , 結果爲true
JS中,類型轉換隻有三種:
轉換具體規則以下:
注意"Boolean 轉字符串"這行結果指的是 true 轉字符串的例子
===叫作嚴格相等,是指:左右兩邊不只值要相等,類型也要相等,例如'1'===1的結果是false,由於一邊是string,另外一邊是number。
==不像===那樣嚴格,對於通常狀況,只要值相等,就返回true,但==還涉及一些類型轉換,它的轉換規則以下:
console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true
複製代碼
對象轉原始類型,會調用內置的[ToPrimitive]函數,對於該函數而言,其邏輯以下:
var obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 輸出7
複製代碼
其實就是上一個問題的應用。
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);//true
複製代碼
紅寶書(p178)上對於閉包的定義:閉包是指有權訪問另一個函數做用域中的變量的函數,
MDN 對閉包的定義爲:閉包是指那些可以訪問自由變量的函數。 (其中自由變量,指在函數中使用的,但既不是函數參數arguments也不是函數的局部變量的變量,其實就是另一個函數做用域中的變量。)
首先要明白做用域鏈的概念,其實很簡單,在ES5中只存在兩種做用域————全局做用域和函數做用域,當訪問一個變量時,解釋器會首先在當前做用域查找標示符,若是沒有找到,就去父做用域找,直到找到該變量的標示符或者不在父做用域中,這就是做用域鏈
,值得注意的是,每個子函數都會拷貝上級的做用域,造成一個做用域的鏈條。 好比:
var a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a);//3
}
}
複製代碼
在這段代碼中,f1的做用域指向有全局做用域(window)和它自己,而f2的做用域指向全局做用域(window)、f1和它自己。並且做用域是從最底層向上找,直到找到全局做用域window爲止,若是全局尚未的話就會報錯。就這麼簡單一件事情!
閉包產生的本質就是,當前環境中存在指向父級做用域的引用。仍是舉上面的例子:
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();
複製代碼
這裏x會拿到父級做用域中的變量,輸出2。由於在當前環境中,含有對f2的引用,f2偏偏引用了window、f1和f2的做用域。所以f2能夠訪問到f1的做用域的變量。
那是否是隻有返回函數纔算是產生了閉包呢?、
回到閉包的本質,咱們只須要讓父級做用域的引用存在便可,所以咱們還能夠這麼作:
var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();
複製代碼
讓f1執行,給f3賦值後,等於說如今f3擁有了window、f1和f3自己這幾個做用域的訪問權限
,仍是自底向上查找,最近是在f1
中找到了a,所以輸出2。
在這裏是外面的變量f3存在着父級做用域的引用
,所以產生了閉包,形式變了,本質沒有改變。
明白了本質以後,咱們就來看看,在真實的場景中,究竟在哪些地方能體現閉包的存在?
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 這就是閉包
fn();
}
// 輸出2,而不是1
foo();
複製代碼
如下的閉包保存的僅僅是window和當前做用域。
// 定時器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件監聽
$('#app').click(function(){
console.log('DOM Listener');
})
複製代碼
全局做用域window
和當前函數的做用域
,所以能夠全局的變量。var a = 2;
(function IIFE(){
// 輸出2
console.log(a);
})();
複製代碼
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
複製代碼
爲何會所有輸出6?如何改進,讓它輸出1,2,3,4,5?(方法越多越好)
由於setTimeout爲宏任務,因爲JS中單線程eventLoop機制,在主線程同步任務執行完後纔去執行宏任務,所以循環結束後setTimeout中的回調才依次執行,但輸出i的時候當前做用域沒有,往上一級再找,發現了i,此時循環已經結束,i變成了6。所以會所有輸出6。
解決方法:
一、利用IIFE(當即執行函數表達式)當每次for循環時,把此時的i變量傳遞到定時器中
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
複製代碼
二、給定時器傳入第三個參數, 做爲timer函數的第一個函數參數
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}
複製代碼
三、使用ES6中的let
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}
複製代碼
let使JS發生革命性的變化,讓JS有函數做用域變爲了塊級做用域,用let後做用域鏈不復存在。代碼的做用域以塊級爲單位,以上面代碼爲例:
// i = 1
{
setTimeout(function timer(){
console.log(1)
},0)
}
// i = 2
{
setTimeout(function timer(){
console.log(2)
},0)
}
// i = 3
...
複製代碼
所以能輸出正確的結果。
在JavaScript中,每當定義一個函數數據類型(普通函數、類)時候,都會天生自帶一個prototype屬性,這個屬性指向函數的原型對象。
當函數通過new調用時,這個函數就成爲了構造函數,返回一個全新的實例對象,這個實例對象有一個__proto__屬性,指向構造函數的原型對象。
JavaScript對象經過prototype指向父類對象,直到指向Object對象爲止,這樣就造成了一個原型指向的鏈條, 即原型鏈。
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);
複製代碼
這樣寫的時候子類雖然可以拿到父類的屬性值,可是問題是父類原型對象中一旦存在方法那麼子類沒法繼承。那麼引出下面的方法。
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();
console.log(new Child2());
複製代碼
看似沒有問題,父類的方法和屬性都可以訪問,但實際上有一個潛在的不足。舉個例子:
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);
複製代碼
能夠看到控制檯:
明明我只改變了s1的play屬性,爲何s2也跟着變了呢?很簡單,由於兩個實例使用的是同一個原型對象。
那麼還有更好的方式麼?
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
複製代碼
能夠看到控制檯:
以前的問題都得以解決。可是這裏又徒增了一個新問題,那就是Parent3的構造函數會多執行了一次(Child3.prototype = new Parent3();)。這是咱們不肯看到的。那麼如何解決這個問題?
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
複製代碼
這裏讓將父類原型對象直接給到子類,父類構造函數只執行一次,並且父類屬性和方法均能訪問,可是咱們來測試一下:
var s3 = new Child4();
var s4 = new Child4();
console.log(s3)
複製代碼
子類實例的構造函數是Parent4,顯然這是不對的,應該是Child4。
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
複製代碼
這是最推薦的一種方式,接近完美的繼承,它的名字也叫作寄生組合繼承。
ES6的代碼最後都是要在瀏覽器上可以跑起來的,這中間就利用了babel這個編譯工具,將ES6的代碼編譯成ES5讓一些不支持新語法的瀏覽器也能運行。
那最後編譯成了什麼樣子呢?
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// ...
//看到沒有
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent () {
// 驗證是不是 Parent 構造出來的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
複製代碼
核心是_inherits函數,能夠看到它採用的依然也是第五種方式————寄生組合繼承方式,同時證實了這種方式的成功。不過這裏加了一個Object.setPrototypeOf(subClass, superClass),這是用來幹啥的呢?
答案是用來繼承父類的靜態方法。這也是原來的繼承方式疏忽掉的地方。
追問: 面向對象的設計必定是好的設計嗎?
不必定。從繼承的角度說,這一設計是存在巨大隱患的。
假如如今有不一樣品牌的車,每輛車都有drive、music、addOil這三個方法。
class Car{
constructor(id) {
this.id = id;
}
drive(){
console.log("wuwuwu!");
}
music(){
console.log("lalala!")
}
addOil(){
console.log("哦喲!")
}
}
class otherCar extends Car{}
複製代碼
如今能夠實現車的功能,而且以此去擴展不一樣的車。
可是問題來了,新能源汽車也是車,可是它並不須要addOil(加油)。
若是讓新能源汽車的類繼承Car的話,也是有問題的,俗稱"大猩猩和香蕉"的問題。大猩猩手裏有香蕉,可是我如今明明只須要香蕉,卻拿到了一隻大猩猩。也就是說加油這個方法,我如今是不須要的,可是因爲繼承的緣由,也給到子類了。
繼承的最大問題在於:沒法決定繼承哪些屬性,全部屬性都得繼承。
固然你可能會說,能夠再建立一個父類啊,把加油的方法給去掉,可是這也是有問題的,一方面父類是沒法描述全部子類的細節狀況的,爲了避免同的子類特性去增長不一樣的父類,代碼勢必會大量重複
,另外一方面一旦子類有所變更,父類也要進行相應的更新,代碼的耦合性過高
,維護性很差。
那如何來解決繼承的諸多問題呢?
用組合,這也是當今編程語法發展的趨勢,好比golang徹底採用的是面向組合的設計方式。
顧名思義,面向組合就是先設計一系列零件,而後將這些零件進行拼裝,來造成不一樣的實例或者類。
function drive(){
console.log("wuwuwu!");
}
function music(){
console.log("lalala!")
}
function addOil(){
console.log("哦喲!")
}
let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
複製代碼
代碼乾淨,複用性也很好。這就是面向組合的設計方式。
參考出處:
更多有趣內容見微信公衆號: