前端進階之路(一):深入理解JavaScript10個概念

前端進階之路(一):深入理解JavaScript10個概念

1.js運行環境

js作為腳本語言運行在瀏覽器中,瀏覽器就是js的運行環境。對於眾多風雲的瀏覽器廠商來說, 他們的內核又是不一樣的。瀏覽器內核分為兩種:渲染引擎和js引擎。

渲染引擎:負責網頁內容呈現的。

Js引擎:解釋js腳本,實現js交互效果的。

1.1常見的內核:

1.2 現在我們有一個js文件,那麼瀏覽器是如何執行它的呢?

首先我們js文件以scirpt標籤元素呈現在html里面的。瀏覽器根據html文件以此解析標籤,當解 析到scirpt標籤時,會停止html解析,阻塞住,開始下載js文件並且執行它,在執行的過程中,如 果是第一個js文件此時瀏覽器會觸發首次渲染(至於為什麼,自己做下實驗,不懂的可以留言)。
所以出現一個問題js文件大大阻礙了html頁面解析及渲染,所以引入async和defer兩個屬性(對於 首屏優化有很大的提升,也要謹慎使用)

async:開啟另外一個線程下載js文件,下載完成,立馬執行。(此時才發生阻塞)

defer:開啟另一個線程下載js文件,直到頁面加載完成時才執行。(根本不阻塞)

2.js數據類型

基本數據類型:

string:由多個16位Unicode字符組成的字符序列,有單引號或雙引號表示

number:採用了IEEE754格式來表示整數和浮點數值

boolean:有兩個字面值,true和false.區分大小寫的

null:只有一個值的數據類型,值為null.表示一個空對象指針,但用typeof操作會返回一個對象。一般 我們把將來用於保存對象的變量初始化為null.

undefined:這個類型只有一個值,在聲明變量未進行賦值時,這個變量的值就是undefined.

Symbol:唯一的值。

引用數據類型:

object:就是一組數據和功能的集合,無序的鍵值對的方式存儲。可以通過new操作符和創建對象構造函數 來創建。常見的對象類型有array,date,function等.

經典面試題:

0.1+0.2為什麼不等於0.3?

0.1和0.2在轉換成二進位後會無限循環,由於標準位數的限制後面多餘的位數會被截掉,此時就已經出現 了精度的損失,相加後因浮點數小數位的限制而截斷的二進位數字在轉換為十進位就會變成 0.30000000000000004。

數據類型檢測方式:

1.typeof

typeof檢測null是一個對象

typeof檢測函數返回時一個function

typeof檢測其他對象都返回 object

2.instanceof(下一節手寫)

只要在當前實例的原型鏈上,用instanceof檢測出來的結果都是true,所以在類的原型繼承中,最後檢測 出來的結果未必是正確的.而且instanceof後面必須更一個對象。
不能檢測基本類型

3. constructor:

每個構造函數的原型對象都有一個constructor屬性,並且指向構造函數本身,由於我們可以手動修改 這個屬性,所以結果也不是很準確。 不能檢測null和undefined

4.Object.prototype.toString.call(最佳方案)

調用Object原型上的toString()方法,並且通過call改變this指向。返回的是字符串

3.js類型轉換

javaScript作為一門弱類型語言,本質為一個變量可以被賦予不同的數據類型。代碼簡潔靈活,但稍有 不慎,會出現很多坑。

javaScript也作為一門動態類型語言,在運行時,可以隨便改變其變量的結構。

所以js變量可以做任意的類型轉換,有兩種方式,顯示類型轉換和隱士類型轉換。

但是能轉換的類型只有三種:to Number,to String,to Boolean.

當基本類型轉換成上述類型時會調用:Number() ,String(), Boolean()

只有” 0 null undefined NaN false轉換boolean為false,其他都為true

當引用類型轉換時,就稍微有些複雜,我們來舉個例子:(所有對象轉換boolean都為true)

let obj={
value:'你好啊',
num:2,
toString:function(){
return this.value
},
valueOf:function(){
return this.num
},
}
console.log(obj+'明天')  //2明天
console.log(obj+1)    // 3
console.log(String(obj))   // 你好啊

當對象進行類型轉換時:

1.首先調用valueOf,如果執行結果是原始值,返回,如果不是下一步

2.其次調用toString,如果執行結果是原始值,返回,如果不是,報錯。

特殊情況:

當使用顯示類型轉換成String時,執行順序則是先調用toString,其次調用valueOf

顯示類型轉換:

Number() / parseFloat() / parseInt()/String() / toString()/Boolean()

隱士類型轉換:

+ – == !><= <= >=

3.1經典面試題:(都能答對真的很厲害了,留個名讓我關注膜拜一下)

1 + '1'
true + 0
{}+[]
4 + {}
4 + [1]
'a' + + 'b'
console.log ( [] == 0 )
console.log ( ! [] == 0 )
console.log ( [] == ! [] )
console.log ( [] == [] )
console.log({} == !{})
console.log({} == {})
一錯就知道,一做又全忘哈哈哈哈。
答案:
'11'
1
"4[object Object]"
"41"
"aNaN"
true
true
true
false
false
false

4.js遍歷

4.1對象遍歷:

1.for in:自身和繼承屬性,可枚舉,不含Symbol

2.Object.keys(obj):可枚舉,不含Symbol,自身

3.Object.values(obj):可枚舉,不含Symbol,自身

4.Object.getOwnPropertyNames(obj):自身所有屬性,不含Symbol

5.Reflect.ownKeys(obj):自身所有屬性

4.2 數組遍歷:

forEach,map,filter,every,some,reduce等.

4.3 字符串遍歷:

for in

4.4 Set數據結構:

Set.prototype.keys():返回鍵名的遍歷器

Set.prototype.values():返回鍵值的遍歷器

Set.prototype.entries():返回鍵值對的遍歷器

Set.prototype.forEach():回調函數遍歷每個成員

4.5 Map數據結構:

Map.prototype.keys():返回鍵名的遍歷器

Map.prototype.values():返回鍵值的遍歷器

Map.prototype.entries():返回鍵值對的遍歷器

Map.prototype.forEach():回調函數遍歷每個成員

5.作用於與作用域鏈

5.1作用域

javascript採用的靜態作用域,也可以稱為詞法作用域,意思是說作用域是在定義的時候就創建了, 而不是運行的時候。此話對於初學者很不好理解,看看下面這個例子:

let a=1
function aa(){
console.log(a)    //輸出1
}
function bb(){
let a=2
aa()
}

是不是非常違背常理啊,你看嘛,aa在bb里面調用的,aa函數里面沒有a變量,那麼就應該去調用它的作 用域里找,剛好找到a等於2。思路是完美的,可是js的作者採用的靜態作用域,不管你們怎麼運行,你們 定義的時候作用域已經生成了。

那麼什麼是作用域?

變量和函數能被有效訪問的區域或者集合。作用域決定了代碼塊之間的資源可訪問性。

作用域也就是一個獨立的空間,用於保護變量防止泄露,也起到隔離作用。每個作用域里的變量可以相同命名,互不干涉。就像一棟房子一樣,每家每戶都是獨立的,就是作用域。

作用域又分為全局作用域和函數作用域,塊級作用域。 全局作用於任何地方都可以訪問到,如window,Math等全局對象。 函數作用域就是函數內部的變量和方法,函數外部是無法訪問到的。 塊級作用域指變量聲明的代碼段外是不可訪問的,如let,const.

5.2作用域鏈

知道作用域後,我們來說說什麼是作用域鏈?

表示一個作用域可以訪問到變量的一個集合。函數作為一個對像有一個[[scope]]屬性,就是表示這個集合的。再來理解幾個概念詞:

AO:活動變量(Active object,VO)

VO:變量對象(Variable object,VO)

執行上下文:代碼運行的環境,分為全局上下文和函數上下文。

舉例子來說明一下:(借用的例子)
// 作者:jianyangdu洋仔
function a() {
function b() {
var b = 234;
}
var a = 123;
b();
}
var gloab = 100;
a();

第一步: a 函數定義

前端進階之路(一):深入理解JavaScript10個概念

我們可以從上圖中看到,a 函數在被定義時,a函數對象的屬性[[scope]]作用域指向他的作用域鏈scope chain,此時它的作用域鏈的第一項指向了GO(Global Object)全局對象,我們看到全局對象上此時有5個屬性,分別是this、window、document、a、glob。

第二步: a 函數執行

前端進階之路(一):深入理解JavaScript10個概念

當a函數被執行時,此時a函數對象的作用域[[scope]]的作用域鏈scope chain的第一項指向了AO(Activation Object)活動對象,AO對象里有4個屬性,分別是this、arguments、a、b。第二項指向了GO(Global Object),GO對象里依然有5個屬性,分別是this、window、document、a、golb。

第三步: b 函數定義

前端進階之路(一):深入理解JavaScript10個概念

! 當b函數被定義時,此時b函數對象的作用域[[scope]]的作用域鏈scope chain的第一項指向了AO(Activation Object)活動對象,AO對象里有4個屬性,分別是this、arguments、a、b。第二項指向了GO(Global Object),GO對象里依然有5個屬性,分別是this、window、document、a、golb。

第四步: b 函數執行

前端進階之路(一):深入理解JavaScript10個概念

當b函數被執行時,此時b函數對象的作用域[[scope]]的作用域鏈scope chain的第一項指向了AO(Activation Object)活動對象,AO對象里有3個屬性,分別是this、arguments、b。第一項指向了AO(Activation Object)活動對象,AO對象里有4個屬性,分別是this、arguments、a、b。第二項指向了GO(Global
Object),GO對象里依然有5個屬性,分別是this、window、document、a、golb。 以上就是上面代碼執行完之後的結果。

6.閉包

引自:https://github.com/mqyqingfeng/Blog/issues/9

不會閉包的程式設計師不是好程式設計師。

閉包的官方定義:

mdn:閉包是指那些能夠訪問自由變量的函數。

維基百科:在函數中可以(嵌套)定義另一個函數時,如果內部的函數引用了外部的函數的變量,則可能產生閉包。閉包可以用來在一個函數與一組「私有」變量之間創建關聯關係。在給定函數被多次調用的過程中,這些私有變量能夠保持其持久性.

我:一個作用域可以訪問另一個作用域的變量,就產生閉包。之前比喻作用域就好比一棟房子每一戶,閉包相當於串門。

什麼是自由變量?

自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。

閉包=函數+函數能夠訪問的自由變量。

從技術的角度講,所有的JavaScript函數都是閉包。

var a = 1;
function foo() {
console.log(a);
}
foo()

foo 函數可以訪問變量 a,但是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數,所以 a 就是自由變量。

那麼,函數 foo + foo 函數訪問的自由變量 a 不就是構成了一個閉包嘛……

我們在看看ECMAScript中,閉包指的是:

1.從理論角度:所有的函數。因為它們都在創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就相當於是在訪問自由變量,這個時候使用最外層的作用域。

2.從實踐角度:以下函數才算是閉包:

即使創建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)

在代碼中引用了自由變量

下面我們開始實踐論證:(例子依然是來自《JavaScript權威指南》)

var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
輸出 "local scope"

因為變量查找的規則是通過作用域鏈的,作用域鏈是在函數定義的時候就已經確定了, 所以我們來看看定義f函數時候的[[scope]]屬性:

[
AO:{
scope:"local scope",
f:function
},
global:{
scope :"local scope",
checkscope:function
}
]

f執行時候的[[scope]]屬性:

[
AO:{
arguments:[],
this:window
},
AO:{
scope:"local scope",
f:function
},
global:{
scope :"local scope",
checkscope:function
}
]
根據先後順序scope變量輸出為"local scope"

經典面試題:

var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案是都是 3,讓我們分析一下原因:

當執行到 data[0] 函數之前,此時全局上下文的 VO 問:

globalContext = {
VO: {
data: [...],
i: 3
}
}

當執行 data[0] 函數的時候,data[0] 函數的作用域鏈為:

data[0]Context = {
Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 並沒有 i 值,所以會從 globalContext.VO 中查找,i 為 3,所以列印的結果就是 3

7.原型及原型對象

不多說,要會背下面這張圖,理解起來很簡單的。

先談一下我的理解吧:

javascript萬物皆對象,每個對象都有一個__proto__屬性,指向了創造它的構造函數的原型對象。

每個函數都有一個原型對象,prototype,當使用new 創造對象時繼承這個對象。

function A(){}
var a=new A()
a.__proto__===A.prototype

下面就有問題了,誰創造了A這個構造函數呢,還有誰創造了A.prototype這個對象呢?

這時候我們就要知道js兩個頂級函數,Function,Object

所有函數都是由Function創建的

A.__proto__===Function.prototype

剛說了所有函數都是由Function創建的,也包括自己。也就是說Function創造了自己:

Function.__proto__===Function.prototype

Object剛講的是頂級函數,所以也是函數:(所有的魚都歸貓管哈哈哈哈哈)

Object.__proto__===Function.prototype

所有的對象都是由Object構造函數創建的:

A.prototype.__proto__===Object.prototype

那麼Object.prototype也是對象啊,是由誰創建的呢,記住萬物皆空,何嘗不是人生,到頭來什麼都會沒有。

Object.prototype.__proto__===null

原型鏈(一種訪問機制):

1.在訪問對象的某個成員的時候會先在對象中找是否存在

2.如果當前對象中沒有就在構造函數的原型對象中找

3.如果原型對象中沒有找到就到原型對象的原型上找

4.直到Object的原型對象的原型是null為止

前端進階之路(一):深入理解JavaScript10個概念

8.this指向問題

這個問題直接從結果入手,this指向一共有七種情況,下面一 一說起。

8.1 全局環境 普通函數調用,普通對象

const obj={a:this}
obj.this===window  //true
function fn(){
console.log(this)   //window
}

8.2 構造函數

  function a() {
console.log(this)
}
const obj = new a()   //  a{}
a()                    // 'window'

new出來的對象,this指向了即將new出來的對象。
當做普通函數執行,this指向window。

8.3 對象方法

  const obj = {
x: 0,
foo: function () {
console.log(this)
}
}
obj.foo()                 // obj
const a = obj.foo
a()                       //window

作為對象方法,this指向了這個對象。(新對象綁定到函數調用的this)
一旦有變量直接指向了這個方法,this為window.

特殊情況

如果在方法里面執行函數,this指向window.

  const obj = {
x: 0,
foo: function () {
console.log(this)      // obj
function foo1() {
console.log(this)    //window
}
foo1()
}
}
obj.foo()   

8.4 構造函數prototype屬性

  function Fn() {
this.a = 10
let a = 100
}
Fn.prototype.fn = function () {
console.log(this.a)             // 10 說明指向了obj這個對象
}
const obj = new Fn()
obj.fn()

原型定義方法的this指向了實例對象。畢竟是通過對象調用的。

8.5 call ,apply, bind

  const obj = {
x: 10
}
function fn() {
console.log(this)
}
fn.call(obj)      //obj
fn.apply(obj) //obj
fn.bind(obj)() //obj

this指向傳入的對象。

8.6 DOM事件

 document.getElementById('app').addEventListener('click', function () {
console.log(this)           // id為app的這個對象
})

指向綁定事件的對象。

8.7 箭頭函數

  obj = {
a: 10,
c: function () {
b = () => {
console.log(this)           //指向obj
}
b()
}
}
obj.c()

在方法中定義函數應該是指向window,但是箭頭函數沒有自己的this,所以指向上一層作用域中的this.

 document.getElementById('app').addEventListener('click', () => {
console.log(this)           // 改為箭頭函數,指向了window,而不是觸發對象
})

8.8 綁定方式:

隱士綁定:

誰調用方法,this指向誰。

顯示綁定

call,bind,apply

new 綁定

優先級問題:

new>顯示綁定>隱式綁定

經典面試題:

上面知道了沒什麼難的。

9.繼承

原型鏈繼承

構造函數繼承

組合繼承

寄生組合繼承

extends繼承

9.1 原型鏈繼承:

 function Animal() {
this.name = 'cat'
this.msg = {
age: 9
}
}
Animal.prototype.greet = function () {
console.log('hehe')
}
function Dog() {
this.name = 'dog'
}
Dog.prototype = new Animal()  //核心一步
const a = new Dog()
a.msg.age = '99'
const b = new Animal()

缺點:多個實例對應用類型操作會被篡改

9.2 構造函數繼承:

function Animal() {
this.name = 'cat'
this.msg = {
age: 9
}
}
Animal.prototype.greet = function () {
console.log('hehe')
}
function Dog() {
Animal.call(this)            // 核心一步
}
const a=new Dog()

缺點:

只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法。

性能不好,每個子類都會擁有父類實例的副本。

9.3 組合繼承:

就是將上兩種方法結合起來

function Animal() {
this.name = 'cat'
this.msg = {
age: 9
}
}
Animal.prototype.greet = function () {
console.log('hehe')
}
function Dog() {
Animal.call(this)            // 核心一步
}
Dog.prototype = new Animal()  // 核心一步
const a=new Dog()

9.4原型式繼承

利用一個空對象作為中介,將某個對象直接賦值給空對象構造函數的原型。

不能做到函數復用

共享引用類型屬性的值

無法傳遞參數 缺點:

function inheritObject(obj){
function F(){};
F.prototype = obj;
return new F();
}
var situation = {
companies:['bigo','yy','uc'];
area:'guangzhou';
}
var situationA = inheritObject(situation);
console.log(situationA.area)     //'guangzhou'

9.5 寄生式繼承

在原型式繼承的基礎上,增強對象,返回構造函數.

缺點同上

 function createAnother(original){
var clone = object(original); // 或 Object.create(original)
clone.sayHi = function(){  // 以某種方式來增強對象
alert("hi");
};
return clone; // 返回這個對象
}
var person = {
name: 'Nicholas',
friends : ["Shelby","Coury","Van"]
}
var anotherPerson  = createAnother(person) 

9.6 extends(es6)

借用阮一峰老師的es6中extends繼承,眼過千遍,不如手寫一遍。

上面的只能說去應付面試,這個才是我們開發中最常用的,所以必須掌握。

寫法:
class Point {
}
class ColorPoint extends Point {
}

上面代碼定義了一個ColorPoint類,該類通過extends關鍵字,繼承了Point類的所有屬性和方法

Object.getPrototypeOf可以使用這個方法判斷,一個類是否繼承了另一個類
Object.getPrototypeOf(ColorPoint) === Point    //true

Super關鍵字

class A {}
class B extends A {
constructor() {
super();
}
}

上面代碼中,子類B的構造函數之中的super(),代表調用父類的構造函數。這是必須的,否則 JavaScript 引擎會報錯。

注意,super雖然代表了父類A的構造函數,但是返回的是子類B的實例,即super內部的this指的是B的實例,因此super()在這里相當於A.prototype.constructor.call(this)。

class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B

上面代碼中,new.target指向當前正在執行的函數。可以看到,在super()執行時,它指向的是子類B的構造函數,而不是父類A的構造函數。也就是說,super()內部的this指向的是B。

作為函數時,super()只能用在子類的構造函數之中,用在其他地方就會報錯。

class A {}
class B extends A {
m() {
super(); // 報錯
}
}

上面代碼中,super()用在B類的m方法之中,就會造成語法錯誤。

第二種情況,super作為對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。

class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();

上面代碼中,子類B當中的super.p(),就是將super當作一個對象使用。這時,super在普通方法之中,指向A.prototype,所以super.p()就相當於A.prototype.p()。

這里需要注意,由於super指向父類的原型對象,所以定義在父類實例上的方法或屬性,是無法通過super調用的。

class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined

上面代碼中,p是父類A實例的屬性,super.p就引用不到它。

如果屬性定義在父類的原型對象上,super就可以取到。

class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();

上面代碼中,屬性x是定義在A.prototype上面的,所以super.x可以取到它的值。

ES6 規定,在子類普通方法中通過super調用父類的方法時,方法內部的this指向當前的子類實例。

class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2

上面代碼中,super.print()雖然調用的是A.prototype.print(),但是A.prototype.print()內部的this指向子類B的實例,導致輸出的是2,而不是1。也就是說,實際上執行的是super.print.call(this)。

由於this指向子類實例,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類實例的屬性。

class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();

上面代碼中,super.x賦值為3,這時等同於對this.x賦值為3。而當讀取super.x的時候,讀的是A.prototype.x,所以返回undefined。

如果super作為對象,用在靜態方法之中,這時super將指向父類,而不是父類的原型對象。

class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2

上面代碼中,super在靜態方法之中指向父類,在普通方法之中指向父類的原型對象。

另外,在子類的靜態方法中通過super調用父類的方法時,方法內部的this指向當前的子類,而不是子類的實例。

10.數據存儲與傳參

在JavaScript中,每一個變量在記憶體中都需要一個空間來存儲。記憶體空間又分為棧記憶體和堆記憶體。 基本數據類型保存在棧中,引用類型保存於堆中。

let a='a'
a='b'
let b=a

首先創建變量a,再創建字符串’a’,使變量a指向’a’,a保存的是這個值

接著又創建字符串’b’,使變量a指向字符串’b’,同時刪除’a’

深拷貝了一份a(重新創建了’b’),使變量b指向了’b’,變量a和變量b互不受影響。

let obj1=new Object()
let obj2=obj1
obj1.name='node'
alert(obj2.name)           //node

創建一個對象,開闢了一個堆記憶體,使變量obj1指向這個對象(堆記憶體)的地址

創建一個變量obj2,將obj1的值賦值給obj2,就是將地址賦值給了obj2,這時obj1和obj2指向的是同一個堆記憶體兩個變量就相互影響。

傳遞參數

所有函數的參數都是按值傳遞的。也就是說,函數外部的值。複製給函數內部的參數.

function add(num){
num+=10
return
}
var counrt=20
var result=add(counrt)
alert(counrt)        //20,沒有變化
alert(result)       //30
function setName(obj){
obj.name='node'
}
var person=new Object()
setName(person)
alert(person.name)       //'node'
function setName(obj){
obj.name='node'
obj={name:'java'}
}
var person=new Object()
setName(person)
alert(person.name)  //'node'

分析:之前說過,函數參數是按值傳遞的。所以在函數內部,obj這個局部變量保存的是person這個對象的地址。第一步操作了這個地址下的name屬性為’node’,第二步,就是將這個變量指向了一個新的堆地址,所以外部person對象絲毫不受影響,name屬性依舊為node.

來源:kknews前端進階之路(一):深入理解JavaScript10個概念