Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
接:https://juejin.im/post/5c6ad9fde51d453c356e37d1
1.JS 的數據類型分類
根據 JavaScript 中的變量類型傳遞方式,分為基本數據類型和引用數據類型。其中基本數據類型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增,表示獨一無二的值),而引用數據類型統稱為Object對象,主要包括對象、數組和函數。在參數傳遞方式上,基本類型是按值傳遞,引用類型是按共享傳遞。
題目:基本類型和引用類型的區別
基本類型和引用類型存儲于內存的位置不同,基本類型直接存儲在棧中,而引用類型的對象存儲在堆中,與此同時,在棧中存儲了指針,而這個指針指向正是堆中實體的起始位置。下面通過一個小題目,來看下兩者的主要區別:
// 基本類型 var a=10 var b=a b=20 console.log(a) // 10 console.log(b) // 20
上述代碼中,a b都是值類型,兩者分別修改賦值,相互之間沒有任何影響。再看引用類型的例子:
// 引用類型 var a={x: 10, y: 20} var b=a b.x=100 b.y=200 console.log(a) // {x: 100, y: 200} console.log(b) // {x: 100, y: 200}
上述代碼中,a b都是引用類型。在執行了b=a之后,修改b的屬性值,a的也跟著變化。因為a和b都是引用類型,指向了同一個內存地址,即兩者引用的是同一個值,因此b修改屬性時,a的值隨之改動
2.數據類型的判斷
1)typeof
typeof返回一個表示數據類型的字符串,返回結果包括:number、boolean、string、symbol、object、undefined、function等7種數據類型,但不能判斷null、array等
2)instanceof
instanceof 是用來判斷A是否為B的實例,表達式為:A instanceof B,如果A是B的實例,則返回true,否則返回false。instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性,但它不能檢測null 和 undefined
3)嚴格運算符===
只能用于判斷null和undefined,因為這兩種類型的值都是唯一的。
4)constructor
constructor作用和instanceof非常相似。但constructor檢測 Object與instanceof不一樣,還可以處理基本數據類型的檢測。 不過函數的 constructor 是不穩定的,這個主要體現在把類的原型進行重寫,在重寫的過程中很有可能出現把之前的constructor給覆蓋了,這樣檢測出來的結果就是不準確的。
5)Object.prototype.toString.call()
Object.prototype.toString.call() 是最準確最常用的方式。
3.淺拷貝與深拷貝
淺拷貝只復制指向某個對象的指針,而不復制對象本身,新舊對象還是共享同一塊內存。
淺拷貝的實現方式(詳見淺拷貝與深拷貝):
深拷貝就是在拷貝數據的時候,將數據的所有引用結構都拷貝一份。簡單的說就是,在內存中存在兩個數據結構完全相同又相互獨立的數據,將引用型類型進行復制,而不是只復制其引用關系。
深拷貝的實現方式:
遞歸實現深拷貝的原理:要拷貝一個數據,我們肯定要去遍歷它的屬性,如果這個對象的屬性仍是對象,繼續使用這個方法,如此往復。
1.執行上下文和執行棧
執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行。 執行上下文的生命周期包括三個階段:創建階段→執行階段→回收階段,我們重點介紹創建階段。
創建階段(當函數被調用,但未執行任何其內部代碼之前)會做以下三件事:
這是因為當函數執行的時候,首先會形成一個新的私有的作用域,然后依次按照如下的步驟執行:
函數多了,就有多個函數執行上下文,每次調用函數創建一個新的執行上下文,那如何管理創建的那么多執行上下文呢?
JavaScript 引擎創建了執行棧來管理執行上下文。可以把執行棧認為是一個存儲函數調用的棧結構,遵循先進后出的原則。
2.作用域與作用域鏈
ES6 到來JavaScript 有全局作用域、函數作用域和塊級作用域(ES6新增)。我們可以這樣理解:作用域就是一個獨立的地盤,讓變量不會外泄、暴露出去。也就是說作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。 在介紹作用域鏈之前,先要了解下自由變量,如下代碼中,console.log(a)要得到a變量,但是在當前的作用域中沒有定義a(可對比一下b)。當前作用域沒有定義的變量,這成為 自由變量。
var a=100 function fn() { var b=200 console.log(a) // 這里的a在這里就是一個自由變量 console.log(b) } fn()
自由變量的值如何得到 —— 向父級作用域(創建該函數的那個父級作用域)尋找。如果父級也沒呢?再一層一層向上尋找,直到找到全局作用域還是沒找到,就宣布放棄。這種一層一層的關系,就是作用域鏈 。
function F1() { var a=100 return function () { console.log(a) } } function F2(f1) { var a=200 console.log(f1()) } var f1=F1() F2(f1) // 100
上述代碼中,自由變量a的值,從函數F1中查找而不是F2,這是因為當自由變量從作用域鏈中去尋找,依據的是函數定義時的作用域鏈,而不是函數執行時。
3.閉包是什么
閉包這個概念也是JavaScript中比較抽象的概念,我個人理解,閉包是就是函數中的函數(其他語言不能這樣),里面的函數可以訪問外面函數的變量,外面的變量的是這個內部函數的一部分。
閉包的作用:
閉包不能濫用,否則會導致內存泄露,影響網頁的性能。閉包使用完了后,要立即釋放資源,將引用變量指向null。
閉包主要有兩個應用場景:
function outer() { var num=0 //內部變量 return function add() { //通過return返回add函數,就可以在outer函數外訪問了。 num++ //內部函數有引用,作為add函數的一部分了 console.log(num) } } var func1=outer() // func1() //實際上是調用add函數, 輸出1 func1() //輸出2 var func2=outer() func2() // 輸出1 func2() // 輸出2
4.this全面解析
先搞明白一個很重要的概念 —— this的值是在執行的時候才能確認,定義的時候不能確認! 為什么呢 —— 因為this是執行上下文環境的一部分,而執行上下文需要在代碼執行之前確定,而不是定義的時候??慈缦吕樱?/p>
// 情況1 function foo() { console.log(this.a) //1 } var a=1 foo() // 情況2 function fn(){ console.log(this); } var obj={fn:fn}; obj.fn(); //this->obj // 情況3 function CreateJsPerson(name,age){ //this是當前類的一個實例p1 this.name=name; //=>p1.name=name this.age=age; //=>p1.age=age } var p1=new CreateJsPerson("尹華芝",48); // 情況4 function add(c, d){ return this.a + this.b + c + d; } var o={a:1, b:3}; add.call(o, 5, 7); // 1 + 3 + 5 + 7=16 add.apply(o, [10, 20]); // 1 + 3 + 10 + 20=34 // 情況5 <button id="btn1">箭頭函數this</button> <script type="text/javascript"> let btn1=document.getElementById('btn1'); let obj={ name: 'kobe', age: 39, getName: function () { btn1.onclick=()=> { console.log(this);//obj }; } }; obj.getName(); </script>
接下來我們逐一解釋上面幾種情況
1.同步 vs 異步
同步,我的理解是一種線性執行的方式,執行的流程不能跨越。比如說話后在吃飯,吃完飯后在看手機,必須等待上一件事完了,才執行后面的事情。
異步,是一種并行處理的方式,不必等待一個程序執行完,可以執行其它的任務。比方說一個人邊吃飯,邊看手機,邊說話,就是異步處理的方式。在程序中異步處理的結果通常使用回調函數來處理結果。
// 同步 console.log(100) alert(200); console.log(300) //100 200 300 // 異步 console.log(100) setTimeout(function(){ console.log(200) }) console.log(300) //100 300 200
2.異步和單線程
JS 需要異步的根本原因是 JS 是單線程運行的,即在同一時間只能做一件事,不能“一心二用”。為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
一個 Ajax 請求由于網絡比較慢,請求需要 5 秒鐘。如果是同步,這 5 秒鐘頁面就卡死在這里啥也干不了了。異步的話,就好很多了,5 秒等待就等待了,其他事情不耽誤做,至于那 5 秒鐘等待是網速太慢,不是因為 JS 的原因。
3.前端異步的場景
前端使用異步的場景
4.Event Loop
一個完整的 Event Loop 過程,可以概括為以下階段:
接下來我們看道例子來介紹上面流程:
Promise.resolve().then(()=>{ console.log('Promise1') setTimeout(()=>{ console.log('setTimeout2') },0) }) setTimeout(()=>{ console.log('setTimeout1') Promise.resolve().then(()=>{ console.log('Promise2') }) },0)
最后輸出結果是Promise1,setTimeout1,Promise2,setTimeout2
1.原型和原型鏈
原型:在JavaScript中原型是一個prototype對象,用于表示類型之間的關系。
原型鏈:JavaScript萬物都是對象,對象和對象之間也有關系,并不是孤立存在的。對象之間的繼承關系,在JavaScript中是通過prototype對象指向父類對象,直到指向Object對象為止,這樣就形成了一個原型指向的鏈條,專業術語稱之為原型鏈。
var Person=function() { this.age=18 this.name='匿名' } var Student=function() {} //創建繼承關系,父類實例作為子類原型 Student.prototype=new Person() var s1=new Student() console.log(s1)
原型關系圖:
當試圖得到一個對象的某個屬性時,如果這個對象本身沒有這個屬性,那么會去它的__proto__(即它的構造函數的prototype)中尋找。如果一直找到最上層都沒有找到,那么就宣告失敗,返回undefined。最上層是什么 —— Object.prototype.__proto__===null
2.繼承
介紹幾種常見繼承方式(如需了解更多,請點擊JavaScript常見的六種繼承方式):
function Parent(value) { this.val=value } Parent.prototype.getValue=function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype=new Parent() const child=new Child(1) child.getValue() // 1 child instanceof Parent // true
以上繼承的方式核心是在子類的構造函數中通過 Parent.call(this) 繼承父類的屬性,然后改變子類的原型為 new Parent() 來繼承父類的函數。
這種繼承方式優點在于構造函數可以傳參,不會與父類引用屬性共享,可以復用父類的函數,但是也存在一個缺點就是在繼承父類函數的時候調用了父類構造函數,導致子類的原型上多了不需要的父類屬性,存在內存上的浪費。
function Parent(value) { this.val=value } Parent.prototype.getValue=function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype=Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true } }) const child=new Child(1) child.getValue() // 1 child instanceof Parent // true
以上繼承實現的核心就是將父類的原型賦值給了子類,并且將構造函數設置為子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的構造函數。
ES6中引入了class關鍵字,class可以通過extends關鍵字實現繼承,還可以通過static關鍵字定義類的靜態方法,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。需要注意的是,class關鍵字只是原型的語法糖,JavaScript繼承仍然是基于原型實現的。
class Parent { constructor(value) { this.val=value } getValue() { console.log(this.val) } } class Child extends Parent { constructor(value) { super(value) this.val=value } } let child=new Child(1) child.getValue() // 1 child instanceof Parent // true
class 實現繼承的核心在于使用 extends 表明繼承自哪個父類,并且在子類構造函數中必須調用 super,因為這段代碼可以看成 Parent.call(this, value)。
1.DOM操作
當網頁被加載時,瀏覽器會創建頁面的文檔對象模型(DOM),我們可以認為 DOM 就是 JS 能識別的 HTML 結構,一個普通的 JS 對象或者數組。接下來我們介紹常見DOM操作:
2.DOM事件模型和事件流
DOM事件模型分為捕獲和冒泡。一個事件發生后,會在子元素和父元素之間傳播(propagation)。這種傳播分成三個階段。
(1)捕獲階段:事件從window對象自上而下向目標節點傳播的階段;
(2)目標階段:真正的目標節點正在處理事件的階段;
(3)冒泡階段:事件從目標節點自下而上向window對象傳播的階段。
DOM事件捕獲的具體流程
捕獲是從上到下,事件先從window對象,然后再到document(對象),然后是html標簽(通過document.documentElement獲取html標簽),然后是body標簽(通過document.body獲取body標簽),然后按照普通的html結構一層一層往下傳,最后到達目標元素。
接下來我們看個事件冒泡的例子:
如何阻止冒泡?
通過event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件處理程序被執行。 我們可以在上例中inner元素的click事件上,添加event.stopPropagation()這句話后,就阻止了父事件的執行,最后只打印了'inner'。
inner.onclick=function(ev) { console.log('inner') ev.stopPropagation() }
3.事件代理(事件委托)
由于事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理。
我們設定一種場景,如下代碼,一個<div>中包含了若干個<a>,而且還能繼續增加。那如何快捷方便地為所有<a>綁定事件呢?
<div id="div1"> <a href="#">a1</a> <a href="#">a2</a> <a href="#">a3</a> <a href="#">a4</a> </div> <button>點擊增加一個 a 標簽</button>
如果給每個<a>標簽一一都綁定一個事件,那對于內存消耗是非常大的。借助事件代理,我們只需要給父容器div綁定方法即可,這樣不管點擊的是哪一個后代元素,都會根據冒泡傳播的傳遞機制,把父容器的click行為觸發,然后把對應的方法執行,根據事件源,我們可以知道點擊的是誰,從而完成不同的事。
最后,使用代理的優點如下:
4.BOM 操作
BOM(瀏覽器對象模型)是瀏覽器本身的一些信息的設置和獲取,例如獲取瀏覽器的寬度、高度,設置讓瀏覽器跳轉到哪個地址。
獲取屏幕的寬度和高度
console.log(screen.width) console.log(screen.height)
獲取網址、協議、path、參數、hash 等
// 例如當前網址是 https://juejin.im/timeline/frontend?a=10&b=10#some console.log(location.href) // https://juejin.im/timeline/frontend?a=10&b=10#some console.log(location.protocol) // https: console.log(location.pathname) // /timeline/frontend console.log(location.search) // ?a=10&b=10 console.log(location.hash) // #some
另外,還有調用瀏覽器的前進、后退功能等
history.back() history.forward()
獲取瀏覽器特性(即俗稱的UA)然后識別客戶端,例如判斷是不是 Chrome 瀏覽器
var ua=navigator.userAgent var isChrome=ua.indexOf('Chrome') console.log(isChrome)
5.Ajax與跨域
Ajax 是一種異步請求數據的一種技術,對于改善用戶的體驗和程序的性能很有幫助。 簡單地說,在不需要重新刷新頁面的情況下,Ajax 通過異步請求加載后臺數據,并在網頁上呈現出來。常見運用場景有表單驗證是否登入成功、百度搜索下拉框提示和快遞單號查詢等等。Ajax的目的是提高用戶體驗,較少網絡數據的傳輸量。
如何手寫 XMLHttpRequest 不借助任何庫
因為瀏覽器出于安全考慮,有同源策略。也就是說,如果協議、域名或者端口有一個不同就是跨域,Ajax 請求會失敗。
那么是出于什么安全考慮才會引入這種機制呢? 其實主要是用來防止 CSRF 攻擊的。簡單點說,CSRF 攻擊是利用用戶的登錄態發起惡意請求。
然后我們來考慮一個問題,請求跨域了,那么請求到底發出去沒有? 請求必然是發出去了,但是瀏覽器攔截了響應。
常見的幾種跨域解決方案:
6.存儲
sessionStorage 、localStorage 和 cookie 之間的區別
作用域:localStorage只要在相同的協議、相同的主機名、相同的端口下,就能讀取/修改到同一份localStorage數據。sessionStorage比localStorage更嚴苛一點,除了協議、主機名、端口外,還要求在同一窗口(也就是瀏覽器的標簽頁)下
生命周期:localStorage 是持久化的本地存儲,存儲在其中的數據是永遠不會過期的,使其消失的唯一辦法是手動刪除;而 sessionStorage 是臨時性的本地存儲,它是會話級別的存儲,當會話結束(頁面被關閉)時,存儲內容也隨之被釋放。
幾種常見模塊化規范的簡介:
CommonJS規范主要用于服務端編程,加載模塊是同步的,這并不適合在瀏覽器環境,因為同步意味著阻塞加載,瀏覽器資源是異步加載的,因此有了AMD CMD解決方案
AMD規范在瀏覽器環境中異步加載模塊,而且可以并行加載多個模塊。不過,AMD規范開發成本高,代碼的閱讀和書寫比較困難,模塊定義方式的語義不順暢。
CMD規范與AMD規范很相似,都用于瀏覽器編程,依賴就近,延遲執行,可以很容易在Node.js中運行。不過,依賴SPM 打包,模塊的加載邏輯偏重
ES6 在語言標準的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規范,成為瀏覽器和服務器通用的模塊解決方案
avaScript有5種基本的數據類型,分別是:布爾、null、undefined、String和Number。這些基本類型在賦值的時候是通過值傳遞的方式。值得注意的是還有另外三種類型: Array、Function和Object,它們通過引用來傳遞。從底層技術上看,它們三都是對象。
如果一個基本的數據類型綁定到某個變量,我們可以認為該變量包含這個基本數據類型的值。
var x=10; var y='abc'; var z=null;
當我們使用=將這些變量賦值到另外的變量,實際上是將對應的值拷貝了一份,然后賦值給新的變量。我們把它稱作值傳遞。
var x=10; var y='abc'; var a=x; var b=y; console.log(x, y, a, b) // 10, 'abc', 10, 'abc'
a和x都包含10,b和y都包含'abc',并且它們是完全獨立的拷貝,互不干涉。如果我們將a的值改變,x不會受到影響。
var x=10; var y='abc'; var a=x; var b=y; a=5; b='def'; console.log(x, y, a, b); // 10, 'abc', 5, 'def'
如果一個變量綁定到一個非基本數據類型(Array, Function, Object),那么它只記錄了一個內存地址,該地址存放了具體的數據。注意之前提到指向基本數據類型的變量相當于包含了數據,而現在指向非基本數據類型的變量本身是不包含數據的。
對象在內存中被創建,當我們聲明arr=[],我們在內存中創建了一個數組。arr記錄的是該內存的地址。
var arr=[]; // (a) arr.push(1); // (b)
當執行完(a)之后,內存中創建了一個空的數組對象,其內存地址為#001, arr 指向該地址。
當執行完(b)之后,數組對象中多了一個元素,但是數組的地址依然沒有變,arr也沒有變。
對象是通過引用傳遞,而不是值傳遞。也就是說,變量賦值只會將地址傳遞過去。
var reference=[1]; var refCopy=reference;
reference和refCopy指向同一個數組。 如果我們更新reference,refCopy也會受到影響。
reference.push(2); console.log(reference, refCopy); // [1, 2], [1, 2]
如果我們將一個已經賦值的變量重新賦值,那么它將包含新的數據或則引用地址。
var obj={ first: 'fundebug.com'}; obj={ second: 'fundebug.cn'};
obj 從指向第一個對象變為指向第二個對象。
如果一個對象沒有被任何變量指向,就如第一個對象(地址為#001),JavaScript引擎的垃圾回收機制會將該對象銷毀并釋放內存。
==和===
對于引用類型的變量,==和===只會判斷引用的地址是否相同,而不會判斷對象具體里屬性以及值是否相同。因此,如果兩個變量指向相同的對象,則返回true。
var arrRef=['Hi!']; var arrRef2=arrRef; console.log(arrRef===arrRef2); // true
如果是不同的對象,及時包含相同的屬性和值,也會返回false。
var arr1=["Hi!"]; var arr2=["Hi!"]; console.log(arr1===arr2); // false
如果想判斷兩個不同的對象是否真的相同,一個簡單的方法就是將它們轉換為字符串然后判斷。
var arr1str=JSON.stringify(arr1); var arr2str=JSON.stringify(arr2); console.log(arr1str===arr2str); // true
另一個方法就是遞歸地判斷每一個屬性的值,直到基本類型位置,然后判斷是否相同。
當我們將基本類型數據傳入函數,函數會將這些數據拷貝賦值給函數的參數變量。
var hundred=100; var two=2; function multiply(x, y) { return x * y; } var twoHundred=multiply(hundred, two);
hundred的值拷貝給變量x,two的值拷貝給變量y。
對于一個函數,給定一個輸入,返回一個唯一的輸出。除此之外,不會對外部環境產生任何附帶影響。我們機會稱該函數為純函數。所有函數內部定義的變量在函數返回之后都被垃圾回收掉。
但是,如果函數的輸入是對象(Array, Function, Object),那么傳入的是一個引用。對該變量的操作將會影響到原本的對象。這樣的編程手法將產生附帶影響,是的代碼的邏輯復雜和可讀性變低。
因此,很多數組函數,比如 Array.map 和 Array.filter 是以純函數的形式實現。雖然它們的參數是一個數組變量,但是通過深度拷貝并賦值給一個新的變量,然后在新的數組上操作,來防止原始數組被更改。
我們來看一個例子:
function changeAgeImpure(person) { person.age=25; return person; } var alex={ name: 'Alex', age: 30 }; var changedAlex=changeAgeImpure(alex); console.log(alex); // { name: 'Alex', age: 25 } console.log(changedAlex); // { name: 'Alex', age: 25 }
在非純函數changeAgeImpure中,將對象person的age更新并返回。原始的alex對象也被影響,age更新為25。
讓我們來看如何實現一個純函數:
function changeAgePure(person) { var newPersonObj=JSON.parse(JSON.stringify(person)); newPersonObj.age=25; return newPersonObj; } var alex={ name: 'Alex', age: 30 }; var alexChanged=changeAgePure(alex); console.log(alex); // { name: 'Alex', age: 30 } console.log(alexChanged); // { name: 'Alex', age: 25 }
我們通過JSON.sringify將對象變為一個字符串,然后再通過JSON.parse將字符串變回對象。通過該操作會生成一個新的對象。
值傳遞和引用傳遞經常在面試中被問到,來嘗試回答一下如下代碼如何輸出:
一陣子利用Balazor開發了一個NuGet站點,對WebAssembly進行了初步的了解,覺得挺有意思。在接下來的一系列文章中,我們將通過實例演示的方式介紹WebAssembly的一些基本概念和編程模式。首先我們先來說說什么是WebAssembly,它主要幫助我們解決什么問題。
本文演示實例下載:app1 app2
一、概述
二、WebAssembly程序(app.wat)
三、編譯生成app.wasm
四、JavaScript程序
五、跑起來看看
六、調用導出函數
WebAssembly可以視為一種采用精簡的”二進制格式”的“低等級”、“類匯編”語言。目前主流的瀏覽器均提供了對WebAssembly的支持,雖然WebAssembly的執行性能(它能夠提供near-native的執行性能)是JavaScript無法比擬的,但是在表達能力和靈活性還是不如JavaScript,所以WebAssembly的出現并不是要取代JavaScript,而是作為JavaScript的“助手”,兩者配合,各自發揮自身的優勢,進而開發出更高質量的Web應用。
與其說WebAssembly像匯編,不如說它更像MSIL,因為它不是面向開發者的高級語言,而是高級語言的“編譯目標”。雖然.NET 的運行時CLR不能直接執行C#、VB.NET和F#這樣的高級語言編寫的代碼,而執行執行MSIL指令,但是我們可以將前者編譯成后者。與之類似,我們也可以使用C、C++、C#、Rust和AssemblyScript(面向WebAssembly的TypeScript)作為開發語言,然后將代碼編譯成WebAssembly。
與匯編一樣,雖然WebAssembly采用的是單純的二進制格式,但是可以轉換成文本形式。不僅如此,我們還可以按照這樣的文本格式來編寫程序(在接下來的內容中,我們提供的WebAssembly程序均采用這樣的編寫方式),并利用相應的工具將其編程成WebAssembly。WebAssembly文本采用一種名為S-expression的樹形結構,我們定義的內容都存在與一個具體的“節點”中,每個節點通過小括號包裹起來,子節點直接內嵌于父節點中。由于模塊是WebAssembly的基本部署和加載單元,所以module總是它們的根節點,如下所示的就是一個合法的最簡單的WebAssembly程序。
(module)
為了讓大家對WebAssembly文本的S-expression有一個大致的了解,我們采用這樣的形勢定義了一個進行整數加法運算的add函數。如下面的代碼片段所示,我們定義的函數通過內嵌于module節點的(func)節點表示。它具有一些子節點,其中(export "add") 表示將這個函數以名稱“add”進行導出,這意味著加載此模塊的JavaScript應用可以直接調用此函數。額外三個節點(param $x i32) (param $y i32) (result i32)定義了函數的簽名,揭示了此函數包含兩個Int32(i32)的輸入參數,返回值(結果)的類型也是Int32(i32)。為了提供可讀性,同時也方面傳參方面,我們將參數進行了命名($x和$y)
(module
(func (export "add") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)
)
值得一提的是,WebAssembly僅僅支持4種數據類型,分別是32和64位的整數(i32和i64)和浮點數(f32和f64),連我們最常使用的字符串類型都不支持。由于字符串本質上就是一組字符序列,而一個字符本質上是采用某種編碼的一段字節序列,所以WebAssembly中針對字符串的處理需要自行解決編解碼的工作,所以直接以S-expression文本的形式編寫WebAssembly程序并不容易。
上面說WebAssembly更像MSIL,還因為執行WebAssembly的VM是一個“堆棧機(Stack Machine)”,意味著它采用“壓?!钡姆绞絺鬟f參數。而匯編面向的是真正的機器語言,是“堆棧機(Stack Machine)”和“寄存器機(Register Machine)”的結合,不僅參數傳遞可以采用堆棧,也可以采用寄存器。這一點可以從add函數的實現看出來,由于最終需要執行i32.add指令,我們需要在這之前調用local.get指令將兩個參數壓入棧中。i32.add執行后的結果也將入棧,并成為函數返回值。
在對WebAssembly以及基于S-expression的文本形式有了基本了解之后,我們通過一個簡單的例子來演示一下一個WebAssembly程序大體上如何編寫,我們著重關注JavaScript應用和WebAssembly之間的功能交互。我們創建一個空的目錄,并創建兩個文本文件app.wat和index.html,前者代表以S-expression文本形式編寫的WebAssembly程序,后者是一個空的HTML頁面,我們利用它提供的JavaScript程序加載并執行編譯后的WebAssembly模塊。
如下所示的是app.wat的內容,看起來很長,好在我們現在對S-expression有了基本的了解,所以理解起來沒有什么問題。模塊的第一個節點(func $print (import "js" "print") (param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同樣是定義了一個函數,我們將其命名為$print, 子節點(import "js" "print") 表明該函數是從作為宿主的Javascript應用中導入的,具體的路徑為js->print。如果導入的Javascript對象通過變量imports表示,意味著imports.js.print表示的就是這個導入的對象。至于其余的四個節點(param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同樣是提供了函數的簽名:個i32類型的參數,分別表示第一個操作數、第二個操作數、操作符(1,2,3,4分別代表加、減、乘、除)和執行結果。由于這個函數僅僅打印運算表達式,所以沒有返回值。
(module
;; import js func print (op1, op2, op, result)
(func $print (import "js" "print")
(param $op1 i32)
(param $op2 i32)
(param $op i32)
(param $result i32))
;; int32 add (int32 x, int32 y)
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)
;; int32 sub(int32 x, int32 y)
(func $sub (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.sub)
;; int32 mul(int32 x, int32 y)
(func $mul (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.mul)
;; int32 div(int32 x, int32 y)
(func $div (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.div_u)
(func $main
;; call print(1, 2, add(1,2), 1)
i32.const 1
i32.const 2
i32.const 1
i32.const 1
i32.const 2
call $add
call $print
;; call print(1, 2, sub(1,2), 2)
i32.const 1
i32.const 2
i32.const 2
i32.const 1
i32.const 2
call $sub
call $print
;; call print(1, 2, mul(1,2), 3)
i32.const 1
i32.const 2
i32.const 3
i32.const 1
i32.const 2
call $mul
call $print
;; call print(1, 2, div(1,2), 4)
i32.const 1
i32.const 2
i32.const 4
i32.const 1
i32.const 2
call $div
call $print
)
(start $main)
)
接下來我們定義了四個進行加、減、乘和除運算的函數add、sub、mul和div,它們與上面定義的add函數類似,不過由于缺少了(export “{funcname}”)節點,所以它們僅僅是四個內部函數而已。接下來,我們定義了一個$main函數,它會傳入相同的參數(1、2)調用上述4個函數,并調用導入的print方法將包含結果的運算表達式打印出來。雖然命名為$main,但是它也僅僅是一個普通的函數而已,所以我們需要利用(start $main)節點將其作為入口函數,這樣它就會在加載的時候自動執行了。
以文本形式編寫的WebAssembly程序需要編譯成二進制模塊才能被加載執行,這里我們使用的wat2wasm這個工具,這個工具可以從這里下載。除了將wat文件轉化成wasm文件的wat2wasm,下載包里還包含了其他一些有用的工具,比如進行反向操作的wasm2wat。上面編寫的app.wat文件利用如下的命令就可以編譯生成WebAssembly目標文件app.wasm。
wat2wasm app.wat -o app.wasm
如下所示的就是index.html文件的內容,我們著重關注其提供的JavaScript代碼。我們首先定義被WebAssembly導入的用來輸出運行表達式的函數print,然后按照導入路徑js.print將其封裝到一個導入對象中({"js":{"print":print}})。我們調用WebAssembly.instantiateStreaming函數以異步方式加載app.wasm模塊,并創建對應的實例。具體下載app.wasm模塊通過第一個參數提供的Promise(fetch("app.wasm"))完成,第二個參數代表導入對象。
<!DOCTYPE html>
<html>
<head></head>
<body>
<script>
var url="app.wasm";
var print=(op1, op2, op, result)=> {
switch (op) {
case 1:
console.log(`${op1} + ${op2}=${result}`);
break;
case 2:
console.log(`${op1} - ${op2}=${result}`);
break;
case 3:
console.log(`${op1} * ${op2}=${result}`);
break;
case 4:
console.log(`${op1} / ${op2}=${result}`);
break;
default:
console.log("invalid operator.");
}
};
WebAssembly
.instantiateStreaming(fetch("app.wasm"),
{"js":{"print":print}});
</script>
</body>
</html>
我們采用相應的方式將當前目錄發布為本地web站點,比如執行Python命令(python -m http.server
),然后我們利用瀏覽器訪問此站點的默認文件index.html,瀏覽器的控制臺輸出就會看到WebAssembly模塊初始化輸出的四個運算表達式。
上面我們演示了WebAssembly模塊在初始化的時候調用導入的JavaScript函數,現在我們來演示JavaScript應用如何調用從WebAssembly導出的函數,為此我們將app.wat的代碼改寫成如下的形式,僅僅保留4個導出的函數add、sub、mul和div。
(module
(func (export "add") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)
(func (export "sub") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.sub)
(func (export "mul") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.mul)
(func (export "div") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.div_u)
)
index.html中的Javascript代碼也做了如下的修改:我們在調用WebAssembly.instantiateStreaming函數成功加載WebAssembly模塊并創建對應模塊實例后,利用返回結果的instance屬性得到這個模塊實例。模塊導出的成員都保存在該實例的exports屬性返回的集合中,為此我們從中提取出導出的四個返回,并利用它們完成對應的運算后,調用console.log函數將包含結果的運算表達式輸出到控制臺上。由于WebAssembly模塊不在需要調用導入的函數,所以調用instantiateStreaming函數的時候不需要在指定導入對象。
<!DOCTYPE html>
<html>
<head></head>
<body>
<script>
var url="app.wasm";
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then(result=> {
var exports=result.instance.exports;
var result=exports.add(1,2);
console.log(`1 + 2=${result}`);
result=exports.sub(1,2);
console.log(`1 - 2=${result}`);
result=exports.mul(1,2);
console.log(`1 * 2=${result}`);
result=exports.div(1,2);
console.log(`1 / 2=${result}`);
});
</script>
</body>
</html>
程序運行之后,瀏覽器的控制臺上依然會輸出相同的結果。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。