在,前端開發腳本基本上是JavaScript一統江湖了。
其實,這只是實踐競爭之后的結果。
估計是為了更好推廣,JavaScript從取名上,蹭了Java語言的熱度。
網景公司在自家瀏覽器上推出JavaScript后,大獲成功;
標準領域也沒閑著,根據JavaScript,形成了ECMAScript標準,并引導了JavaScript后續發展。
微軟的IE瀏覽器為了跟網景競爭,推出了JScript和VBScript。其中JScript是IE對ECMAScript的實現。
雖然語法基本相同,但由于瀏覽器實現差異不小,前端開發者不得不做痛苦的兼容。
那年頭,微軟自家的VB也很常見,VBScript也是一種選擇。
谷歌出現了,推出了Chrome瀏覽器,實現了V8引擎,能夠更高效的運行JavaScript。
隨著Chrome的流行,JavaScript的支持率越來越高,前端腳本向JavaScript快速集中。
再然后,Node.js出現了。基于 Chrome V8 引擎,初衷是讓Javascript可以在更多環境中運行,實際上是極大方便了JavaScript開發。
至此,JavaScript已經非常強大了。大家都是在JavaScript基礎上做文章,試圖讓開發者更爽。不管是用Vue,還是Angular、React,都離不開JavaScript。
這期間,微軟推出了TypeScript。是的,又是微軟。
TypeScript只是JavaScript語法超集,而且打包之后給瀏覽器運行的依然是JavaScript。
其他場景下,Javascript也得到了應用。
H5、小程序由于是基于瀏覽器而來,自然把JavaScript作為主要支持。
華為的鴻蒙HarmonyOS操作系統,在界面開發上,選擇了同時支持Java和JavaScript開發。額,近期在TypeScript基礎上,又推出了eTS開發語言。
走到今天,JavaScript可以說是主流了。
如果初學前端開發,掌握JavaScript是比較保險的選擇。
順著它的發展梳理下來,也可以快速了解這個領域。
HTML、CSS、Javascript是前端Web開發的主要語言,但是學生們經?;煜齁avascript和HTML。因此,今天我們在這里討論這兩種最重要的編程語言之間的差異。
Javascript簡介
Javascript 是一種基于文本的編程語言,主要用于Web開發。它是WWW(萬維網)的核心技術之一。
通過使用 Javascript,程序員可以創建交互式和響應式Web元素并增強網站的UI。Javascript 在客戶端Web開發中非常流行,以至于互聯網上超過97%的網站都將其用于客戶端行為。彈出窗口、圖像滑塊、站點導航菜單、選項卡、表單驗證是用于Web開發的Javascript的一些示例。
HTML簡介
HTML是超文本標記語言的首字母縮寫,它是用于創建網頁和Web應用程序的標準標記語言。HTML描述了網頁的結構,瀏覽器從服務器接收HTML頁面,然后將文檔呈現到屏幕上。
HTML包含多個元素,這些元素告訴瀏覽器內容的外觀。例如,<p>標簽定義了一個段落,<title>定義了web文檔的標題,<body>定義了可見的頁面內容,<table>定義了一個表格等等。HTML可以嵌入腳本程序如JS程序 會影響網頁的內容和行為。此外,它可以包含 CSS(層疊樣式表)來改善網頁的布局和外觀。
Javascript與HTML:主要區別是什么?
句法
HTML是一種標記語言,它有多種標簽,如表格標簽、換行標簽、圖片標簽、標題標簽等。同時,Javascript具有不同的功能,可以使網頁具有交互性和動態性。HTML語法更容易學習和理解。與HTML相比,Javascript是一種復雜的語言。
動態規劃
Javascript使網站動態化,它可以更改用戶操作的HTML標記的值和屬性,它主要包含在HTML代碼中以制作交互式網頁。
另一方面,僅HTML無法制作動態網頁,因此,無法動態更改代碼。程序員可以在HTML代碼中包含Javascript來制作動態網頁。
兼容性
兼容性是Javascript與HTML的主要關注問題之一。每個瀏覽器都支持HTML,它的標簽可以在每個瀏覽器上毫無問題地呈現,但是,Javascript并不總是與每個瀏覽器兼容。
例如,一些Javascript函數在Chrome或Firefox中運行良好,但在Internet Explorer 中運行不佳,這就是為什么一些網站提到兼容瀏覽器列表以確保在客戶端瀏覽器中正確查看網站。
客戶端和服務器端Web開發
HTML用于對網頁的客戶端進行編碼,程序員不能使用它來編寫服務器端代碼。相比之下,Javascript可以在網站的客戶端和服務器端使用,服務器端 Javascript(SSJS)是核心javascript的擴展版本。
例如,NodeJS是一個開源和跨平臺的Javascript運行時環境,允許程序員使用 javascript編寫服務器端代碼。在NodeJS的幫助下,Javascript代碼可以在瀏覽器之外運行。
JavaScript與HTML的比較
總結
我們已經討論了有關Javascript與HTML的所有內容。HTML用于創建網頁,它由瀏覽器解釋并以文本和圖像的形式顯示在屏幕上,而javascript主要用于使網頁具有交互性。Javascript是一種比HTML更先進、更復雜的語言,這兩種語言一起使用來制作優秀的網站。
者 | 司徒正美
責編 | 郭芮
出品 | CSDN(ID:CSDNnews)
JavaScript能發展到現在的程度已經經歷不少的坎坷,早產帶來的某些缺陷是永久性的,因此瀏覽器才有禁用JavaScript的選項。甚至在jQuery時代有人問出這樣的問題,jQuery與JavaScript哪個快?在Babel.js出來之前,發明一門全新的語言代碼代替JavaScript的呼聲一直不絕于耳,前有VBScript,Coffee, 后有Dartjs, WebAssembly。要不是它是所有瀏覽器都內置的腳本語言, 可能就命絕于此。瀏覽器就是它的那個有錢的丈母娘。此外源源不斷的類庫框架,則是它的武器庫,從底層革新了它自己。為什么這么說呢?
JavaScript沒有其他語言那樣龐大的SDK,針對某一個領域自帶的方法是很少,比如說數組方法,字符串方法,都不超過20個,是Prototype.js給它加上的。JavaScript要實現頁面動效,離不開DOM與BOM,但瀏覽器互相競爭,導致API不一致,是jQuery搞定了,還帶來了鏈式調用與IIFE這些新的編程技巧。在它缺乏大規模編程模式的時候,其他語言的外來戶又給它帶來了MVC與MVVM……這里面許多東西,久而久之都變成語言內置的特性,比如Prototype.js帶來的原型方法,jQuery帶來的選擇器方法,實現MVVM不可缺少的對象屬性內省機制(getter, setter, Reflect, Proxy), 大規模編程需要的class, modules。
本文將以下幾個方面介紹這些新特性,正是它們武裝了JavaScript,讓它變成一個正統的,魔幻的語言。
原型方法的極大豐富;
類與模塊的標準化;
異步機制的嬗變;
塊級作用域的補完;
基礎類型的增加;
反射機制的完善;
更順手的語法糖。
原型方法的極大豐富
原型方法自Prototype.js出來后,就不斷被招安成官方API?;旧显谧址c數組這兩大類別擴充,它們在日常業務中不斷被使用,因此不斷變重復造輪子,因此企待官方化。
JavaScript的版本說明:
這些原型方法非常有用,以致于在面試中經常被問到,如果去除字符串兩邊的空白,如何扁平化一個數組?
類與模塊的標準化
在沒有類的時代,每個流行框架都會帶一個創建類的方法,可見大家都不太認同原型這種復用機制。
下面是原型與類的寫法比較:
function Person(name) {
this.name = name;
}
//定義一個方法并且賦值給構造函數的原型
Person.prototype.sayName = function {
return this.name;
};
var p = new Person('ruby');
console.log(p.sayName) // ruby
class Person {
constructor(name){
this.name = name
}
sayName {
return this.name;
}
}
var p = new Person('ruby');
console.log(p.sayName) // ruby
我們可以看到es6的定義是非常簡單的,并且不同于對象鍵值定義方式,它是使用對象簡寫來描述方法。如果是標準的對象描述法,應該是這樣:
//下面這種寫法并不合法
class Person {
constructor: function(name){
this.name = name
}
sayName: function {
return this.name;
}
}
如果我們想繼承一個父類,也很簡單:
class Person extends Animal {
constructor: function(name){
super;
this.name = name
}
sayName: function {
return this.name;
}
}
此外,它后面還補充了三次相關的語法,分別是屬性初始化語法,靜態屬性與方法語法,私有屬性語法。目前私有屬性語法爭議非常大,但還是被標準化。雖然像typescript的private、public、protected更符合從后端轉行過來的人的口味,不過在babel無所不能的今天,我們完全可以使用自己喜歡的寫法。
與類一起出現的還有模塊,這是一種比類更大的復用單元,以文件為載體,可以實現按需加載。當然它最主要的作用是減少全局污染。jQuery時代,通過IIFE減少了這癥狀,但是JS文件沒有統一的編寫規范,意味著想把它們打包一個是非常困難的,只能像下面那樣平鋪著。這些文件的依賴關系,只有最初的人知道,要了幾輪開發后,就是定時炸彈。此外,不要忘記,<script>
標準還會導致頁面渲染堵塞,出現白屏現象。
<script src="zepto.js"></script>
<script src="jhash.js"></script>
<script src="fastClick.js"></script>
<script src="iScroll.js"></script>
<script src="underscore.js"></script>
<script src="handlebar.js"></script>
<script src="datacenter.js"></script>
<script src="util/wxbridge.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>
于是后jQuery時代,國內流行三種模塊機制,以seajs主體的CMD,以requirejs為主體的AMD,及nodejs自帶的Commonjs。當然,后來還有一種三合一方案UMD(AMD, Commonjs與es6 modules)。
requirejs的定義與使用:
define(['jquery'], function($){
//some code
var mod = require("./relative/name");
return {
//some code
} //返回值可以是對象、函數等
})
require(['cores/cores1', 'cores/cores2', 'utils/utils1', 'utils/utils2'], function(cores1, cores2, utils1, utils2){
//some code
})
requirejs是世界第一款通用的模塊加載器,尤其自創了shim機制,讓許多不模范的JS文件也可以納入其加載系統。
define(function(require){
var $ = require("jquery");
$("#container").html("hello,seajs");
var service = require("./service")
var s = new service;
s.hello;
});
//另一個獨立的文件service.js
define(function(require,exports,module){
function Service{
console.log("this is service module");
}
Service.prototype.hello = function{
console.log("this is hello service");
return this;
}
module.exports = Service;
});
Seajs是阿里大牛玉伯加的加載器,借鑒了Requiejs的許多功能,聽說其性能與嚴謹性超過前者。當前為了正確分析出define回調里面的require語句,還發起了一個 100 美刀賞金活動,讓國內高手一展身手。
https://github.com/seajs/seajs/issues/478
image_1doan2vfl17ld1nin1hbm182c9b9p.png-72.9kB
相對而言,nodejs模塊系統就簡單多了,它沒有專門用于包裹用戶代碼的define方法,它不需要顯式聲明依賴。
//world.js
exports.world = function {
console.log('Hello World');
}
//main.js
let world = require('./world.js')
world;
function Hello {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function {
console.log('Hello ' + name);
};
};
module.exports = Hello;
而官方欽點的es6 modules與nodejs模塊系統極其相似,只是將其方法與對象變成關鍵字。
//test.js或test.mjs
import * as test from './test';
//aaa.js或aaa.mjs
import {aaa} from "./aaa"
const arr = [1, 2, 3, 4];
const obj = {
a: 0,
b: function {}
}
export const foo = => {
const a = 0;
const b = 20;
return a + b;
}
export default {
num,
arr,
obj,
foo
}
那怎么使用呢?根據規范,瀏覽器需要在link標簽與script標簽添加新的屬性或屬性值來支持這新特性。(詳見:https://www.jianshu.com/p/f7db50cf956f)
<link rel="modulepreload" href="lib.mjs">
<link rel="modulepreload" href="main.mjs">
<script type="module" src="main.mjs"></script>
<script nomodule src="fallback.js"></script>
但可惜的是,瀏覽器對模塊系統的支持是非常滯后,并且即便最新的瀏覽器支持了,我們還是免不了要兼容舊的瀏覽器。對此,我們只能奠出webpack這利器,它是前端工程化的集大成者,可以將我們的代碼通過各種loader/plugin打包成主流瀏覽器都認識的JavaScript語法,并以最原始的方式掛載進去。
異步機制的嬗變
在JavaScript沒有大規模應用前,用到異步的地方只有ajax請求與動畫,在請求結束與動畫結束時要做什么事,使用的辦法是經典的回調。
回調
由于javascript是單線程的,我們的方法是同步的,像下面這樣,一個個執行:
A;
B;
C;
而異步則是不可預測其觸發時機:
A;
// 在現在發送請求
ajax({
url: url,
data: {},
success:function(res){
// 在未來某個時刻執行
B(res)
}
})
C;
//執行順序:A -> C -> B
回調函數是主函數的后繼方法,基本上能保證,主函數執行后,它能在之后某個時刻被執行一次。但隨著功能的細分,在微信小程序或快應用中,它們拆分成三個,即一個方法跟著三個回調。
// https://doc.quickapp.cn/features/system/share.html
import share from '@system.share'
share.share({
type: 'text/html',
data: '<b>bold</b>',
success: function{},
fail: function{},
complete: function{}
})
在nodejs中,內置的異步方法都是使用一種叫Error-first回調模式。
fs.readFile('/foo.txt', function(err, data) {
// TODO: Error Handling Still Needed!
console.log(data);
});
在后端,由于存在IO操作,異步操作非常多,異步套異步很容易造成回調地獄。于是出現了另一種模式,事件中心,EventBus或EventEmiiter。
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter;
ee.on('some_events', function(foo, bar) {
console.log("第1個監聽事件,參數foo=" + foo + ",bar="+bar );
});
console.log('第一輪');
ee.emit('some_events', 'Wilson', 'Zhong');
console.log('第二輪');
ee.emit('some_events', 'Wilson', 'Z');
事件可以一次綁定,多次觸發,并且可以將原來內部的回調拖出來,有效地避免了回調地獄。但事件中心,對于同一種行為,總是解發一種回調,不能像小程序的回調那么清晰。于是jQuery引進了Promise。
Promise
Promise最初叫Deffered,從Python的Twisted框架中引進過來。它通過異步方式完成用類的構建,又通過鏈式調用解決了回調地獄問題。
var p = new Promise(function(resolve, reject){
console.log("========")
setTimeout(function{
resolve(1)
},300)
setTimeout(function{
//reject與resolve只能二選一
reject(1)
},400)
});
console.log("這個先執行")
p.then(function (result) {
console.log('成功:' + result);
})
.catch(function (reason) {
console.log('失?。? + reason);
}).finally(function{
console.log("總會執行")
})
為什么這么說呢?看上面的示例,new Promise(executor)
里的executor方法,它會待到then, catch, finally等方法添加完,才會執行,它是異步的。而then, catch, finally則又恰好對應success, fail, complete這三種回調,我們可以為Promise以鏈式方式添加多個then方法。
如果你不想寫catch,新銳的瀏覽器還提供了一個新事件做統一處理:
window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - 產生錯誤的 promise
alert(event.reason); // Error: Whoops! - 未處理的錯誤對象
});
new Promise(function {
throw new Error("Whoops!");
}); // 沒有 catch 處理錯誤
nodejs也有相同的事件:
process.on('unhandledRejection', (reason, promise) => {
console.log('未處理的拒絕:', promise, '原因:', reason);
// 記錄日志、拋出錯誤、或其他邏輯。
});
除此之外,esma2020年還為Promise添加了三個靜態方法:Promise.all和Promise.race,Promise.allSettled 。
其實chrome 60已經都可以用了。
Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數內所有的 promise 都“完成(resolved)”或參數中不包含 promise 時回調完成(resolve);如果參數中 promise 有一個失?。╮ejected),此實例回調失?。╮eject),失敗原因的是第一個失敗 promise 的結果。
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
這個方法類似于jQuery.when,專門用于處理并發事務。
Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。此方法用于競態的情況。
Promise.allSettled(iterable)方法返回一個promise,該promise在所有給定的promise已被解析或被拒絕后解析,并且每個對象都描述每個promise的結果。它類似于Promise.all,但不會因為一個reject就會執行后繼回調,必須所有promise都被執行才會。
Promise不并比EventBus, 回調等優異,但是它給前端API提供了一個標杠,以后處理異步就是返回一個Promise。為后來async/await做了鋪墊。
生成器
生成器generator, 不是為解決異步問題而誕生的,只是恰好它的某個特性可以解耦異步的復雜性,加之koa的暴紅,人們發現原來generator還可以這樣用,于是就火了。
為了理解生成器的含義,我們需要先了解迭代器,迭代器中的迭代就是循環的意思。比如es5中的forEach, map, filter就是迭代器。
let numbers = [1, 2, 3];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
//它比上面更精簡
numbers.forEach(function(el){
console.log(el);
})
但forEach會一下子把所有元素都遍歷出來,而我們喜歡一個個處理呢?那我們就要手寫一個迭代器。
function makeIterator(array){
var nextIndex = 0;
return {
next: function{
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
var it = makeIterator([1,2,3])
console.log(it.next); // {value: 1, done: false}
console.log(it.next); // {value: 2, done: false}
console.log(it.next); // {value: 3, done: false}
console.log(it.next); // {done: true}
而生成器則將創建迭代器常用的模式官方化,就像創建類一樣,但是它寫法有點怪,不像類那樣專門弄一個關鍵字,也沒有像Promise那樣弄一個類。
//理想中是這樣的
Iterator{
exector{
yield 1;
yield 2;
yield 3;
}
}
//現實是這樣的
function* Iterator {
yield 1;
yield 2;
yield 3;
}
其實最好是像Promise那樣,弄一個類,那么我們還可以用現成的語法來模擬,但生成器,現在一個新關鍵字yield,你可以將它當一個return語句。生成器執行后,會產生一個對象,它有一個next方法,next方法執行多少次,就輪到第幾個yield的值返回。
function* Iterator {
yield 1;
yield 2;
yield 3;
}
let it = Iterator;
console.log(it.next); // {value: 1, done: false}
console.log(it.next); // {value: 2, done: false}
console.log(it.next); // {value: 3, done: false}
console.log(it.next); // {value: undefined, done: true}
由于寫法比較離經背道,因此通常見于類庫框架,業務中很少有人使用。它涉及許多細節,比如說yield與return的混用。
function* generator {
yield 1;
return 2; //這個被轉換成 yield 2, 并立即設置成done: true
yield 3; //這個被忽略
}
let it = generator;
console.log(it.next); // {value: 1, done: false}
console.log(it.next); // {value: 2, done: true}
console.log(it.next); // {value: undefined, done: true}
image_1doda17jkj7kl4u1qru1era2m316.png-322.9kB
但說了這么多,這與異步有什么關系呢?我們之所以需要回調,事件,Promise這些,其實是希望能實現以同步代碼的方式組件異步邏輯。yield相當一個斷點,能中斷程序往下執行。于是異步的邏輯就可以這樣寫:
function* generator {
yield setTimeout(function{ console.log("111"), 200})
yield setTimeout(function{ console.log("222"), 100})
}
let it = generator;
console.log(it.next); // 1 視瀏覽器有所差異
console.log(it.next); // 2 視瀏覽器有所差異
如果沒有yield,肯定是先打出222,再打出111。
好了,我們搞定異步代碼以同步代碼的順序輸出后,就處理手動執行next方法的問題。這個也簡單,寫一個方法,用程序執行它們。
function timeout(data, time){
return new Promise(function(resolve){
setTimeout(function{
console.log(data, new Date - 0)
resolve(data)
},time)
})
}
function *generator{
let p1 = yield timeout(1, 2000)
console.log(p1)
let p2 = yield timeout(2, 3000)
console.log(p2)
let p3 = yield timeout(3, 2000)
console.log(p3)
return 2;
}
// 按順序輸出 1 2 3
/* 傳入要執行的gen */
/* 其實循環遍歷所有的yeild (函數的遞歸)
根絕next返回值中的done判斷是否執行到最后一個,
如果是最后一個則跳出去*/
function run(fn) {
var gen = fn;
function next(data) {
// 執行gen.next 初始data為undefined
var result = gen.next(data)
// 如果result.done 為true
if(result.done) {
return result.value
}else{
// result.value 為promise
result.value.then(val=>{
next(val)
})
}
}
// 調用上一個next方法
next;
}
run(generator)
koa早些年的版本依賴的co庫,就是基于上述原理擺平異步問題。有興趣的同學可以下來看看。
async/await
上節章的生成器已經完美地解決異步的邏輯以同步的代碼編寫的問題了,什么異常,可以直接try catch,成功則直接往下走,總是執行可以加finally語句,美中不足是需要對yield后的方法做些改造,改成Promise(這個也有庫,在nodejs直接內置了util.promisefy)。然后需要一個run方法,代替手動next。于是處于語言供應鏈上流的大佬們想,能不能直接將這兩步內置呢?然后包裝一個已經被人接受的語法提供給沒有見過世面的前端工程師呢?他們搜刮了一遍,還真有這東西。那就是C#有async/await。
//C# 代碼
public static async Task<int> AddAsync(int n, int m) {
int val = await Task.Run( => Add(n, m));
return val;
}
這種沒有學習成本的語法很快遷移到JS中,async關鍵字,相當于生成器函數與我們自造的執行函數,await關鍵字相當于yield,但它只有在它跟著的是Promise才會中斷流程執行。async函數最后會返回一個Promise,可以供外面的await關鍵字使用。
//javascript 代碼
async function addTask {
await new Promise(function(resolve){
setTimeout(function{ console.log("111"); resolve, 200})
})
console.log('222')
await new Promise(function(resolve){
setTimeout(function{ console.log("333"); resolve, 200})
})
console.log('444')
}
var p = addTask
console.log(p)
image_1dodd79nc1imnnm91q1b1p7qhdp1j.png-6.1kB
在循環中使用async/await:
const array = ["a","b", "c"]
function getNum(num){
return new Promise(function(resolve){
setTimeout(function{
resolve(num)
}, 300)
})
}
async function asyncLoop {
console.log("start")
for(let i = 0; i < array.length; i++){
const num = await getNum(array[i]);
console.log(num, new Date-0)
}
console.log("end")
}
asyncLoop
async函數里面的錯誤也可以用try catch包住,也可以使用上面提到的unhandledrejection方法。
async function addTask {
try{
await ...
console.log('222')
}catch(e){
console.log(e)
}
}
此外,es2018還添加了異步迭代器與異步生成器函數,讓我們處理各種異步場景更加得心應手:
//異步迭代器
const ruby = {
[Symbol.asyncIterator]: => {
const items = [`r`, `u`, `b`, `y`, `l`, `o`,`u`, `v`, `r`, `e`];
return {
next: => Promise.resolve({
done: items.length === 0,
value: items.shift
})
}
}
}
for await (const item of ruby) {
console.log(item)
}
//異步生成器函數,async函數與生成器函數的混合體
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine;
}
} finally {
await file.close;
}
}
塊級作用域的補完
說起作用域,大家一般認為JavaScript只有全局作用域與函數作用域,但是es3時代,它還是能通過catch語句與with語句創造塊級作用域的。
try{
var name = 'global' //全局作用域
}catch(e){
var b = "xxx"
console.log(b)//xxx
}
console.log(b)
var obj = {
name: "block"
}
with(obj) {
console.log(name);//Block塊上的name block
}
console.log(name)//global
但是catch語句執行后,還是會污染外面的作用域,并且catch是很耗性能的。而with更不用說了,會引起歧義,被es5嚴格模式禁止了。
話又說回來,之所以需要塊狀作用域,是用來解決es3的兩個不好的設計,一個是變量提升,一個重復定義,它們都不利于團隊協作與大規模生產。
var x = 1;
function rain{
alert( x ); //彈出 'undefined',而不是1
var x = 'rain-man';
alert( x ); //彈出 'rain-man'
}
rain;
因此到es6中,新添了let和const關鍵字來實現塊級作用域。這兩個關鍵字相比var,有如下特點:
作用域是局部,作用范圍是括起它的兩個花括號間,即for{}
,while{}
,if{}
與單純的{}
。
它也不會提升到作用域頂部,它頂部到定義的那一行變稱之為“暫時性死區”,這時使用它會報錯。
變量一旦變let, const聲明,就再不能重復定義,否則也報錯。這種嚴格的錯誤提示對我們調試是非常有幫助的。
let a = "hey I am outside";
if(true){
//此處存在暫時性死區
console.log(a);//Uncaught ReferenceError: a is not defined
let a = "hey I am inside";
}
//let與const不存在變量提升
console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // Uncaught ReferenceError: b is not defined
let a = 1; //Uncaught SyntaxError: Identifier 'a' has already been declared
const b = 2;
//不存在變量提升,因此塊級作用域外層無法訪問
if(true){
var bar = "bar";
let baz = "baz";
const qux = "qux";
}
console.log(bar);//bar
console.log(baz);//baz is not defined
console.log(qux);//qux is not defined
const聲明則比let聲明多了一個功能,就讓目標變量的值不能再次改變,即其他語言的常量。
基礎類型的增加
在javascript, 我們通過typeof與Object.prototype.toString.call可以區分出對象的類型,過去總有7種類型:undefined, , string, number, boolean, function, object?,F在又多出兩個類型,一個是es6引進的Symbol,另一個是es2019的bBigInt。
console.log(typeof 9007199254740991n); // "bigint"
console.log(typeof Symbol("aaa")); // "symbol"
Symbol擁有三個特性,創建的值是獨一無二的,附加在對象是不可遍歷的,不支持隱式轉換。此外Symbol上面還有其他靜態方法,用來為對象擴展更多功能。
我們先看它如何表示獨一無二的屬性值。如果沒有Symbol,我們尋常表示常量的方法是不可靠的。
const COLOR_GREEN = 1
const COLOR_RED = 2
const LALALA = 1;
function isSafe(args) {
if (args === COLOR_RED) return false
if (args === COLOR_GREEN) return true
throw new Error(`非法的傳參: ${args}`)
}
console.log(isSafe(COLOR_GREEN)) //true
console.log(isSafe(COLOR_RED)) //false
console.log(isSafe(LALALA)) //true
如果是Symbol,則符合我們的預期:
const COLOR_GREEN = Symbol("1")//傳參可以是字符串,數字,布爾或不填
const COLOR_RED = Symbol("2")
const LALALA = Symbol("1")
function isSafe(args) {
if (args === COLOR_RED) return false
if (args === COLOR_GREEN) return true
throw new Error(`非法的傳參: ${args}`)
}
console.log(isSafe(COLOR_GREEN)) //true
console.log(isSafe(COLOR_RED)) //false
console.log(COLOR_GREEN == LALALA) //false
console.log(isSafe(LALALA)) //throw error
注意,Symbol不是一個構造器,不能new。
new Symbel("222")
會拋錯。
第二點,過往的對象屬性都是字符串類型,如果我們沒有用Object.defineProperty做處理,它們都能直接用for in
遍歷出來。而Symbol屬性不一樣,遍歷不出來,因此適用做對象的私有屬性,因為你只有知道它的名字,才能訪問到它。
var a = {
b: 11,
c: 22
}
var d = Symbol;
a[d] = 33
for(var i in a){
console.log(i, a[i]) //只有b,c
}
第三點,以往的數據類型都可以與字符串相加,變成一個字符串,或者減去一個數字,隱式轉換為數字;而Symbol則直接拋錯。
ar d = Symbol("11")
console.log(d - 1)
我們再來看它的靜態方法:
Symbol.for
這類似一個Symbol, 但是它不表示獨一無二的值,如果用Symbor.for創建了一個symbol, 下次再用相同的參數來訪問,是返回相同的symbol。
Symbol.for("foo"); // 創建一個 symbol 并放入 symbol 注冊表中,鍵為 "foo"
Symbol.for("foo"); // 從 symbol 注冊表中讀取鍵為"foo"的 symbol
Symbol.for("bar") === Symbol.for("bar"); // true,證明了上面說的
Symbol("bar") === Symbol("bar"); // false,Symbol 函數每次都會返回新的一個 symbol
var sym = Symbol.for("mario");
sym.toString;
上面例子是從火狐官方文檔拿出來的,提到注冊表這樣的東西,換言之,我們所有由Symbol.for創建的symbol都由一個內部對象所管理。
Symbol.keyFor
Symbol.keyFor方法返回一個已注冊的 symbol 類型值的key。key就是我們的傳參,也等于同于symbol的description屬性。
let s1 = Symbol.for("111");
console.log( Symbol.keyFor(s1) ) // "111"
console.log(s1.description) // "111"
let s2 = Symbol("222");
console.log( Symbol.keyFor(s2)) // undefined
console.log(s2.description) // "222"
let s3 = Symbol.for(111);
console.log( Symbol.keyFor(s3) ) // "111"
console.log(s3.description) // "111"
需要注意的是,Symbol.for為 Symbol 值登記的名字,是全局環境的,可以在不同的 iframe 或 service worker 中取到同一個值。
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('111') === Symbol.for('111')// true
Symbol.iterator
在es6中添加了for of
循環,相對于for in
循環,它是直接遍歷出值。究其原因,是因為數組原型上添加Symbol.iterator,它就是一個內置的迭代器,而for of
就是執行函數的語法。像數組,字符串,arguments, NodeList, TypeArray, Set, Map, WeakSet, WeatMap的原型都加上Symbol.iterator,因此都可以用for of
循環。
console.log(Symbol.iterator in new String('sss')) // 將簡單類型包裝成對象才能使用in
console.log(Symbol.iterator in [1,2,3] )
console.log(Symbol.iterator in new Set(['a','b','c','a']))
for(var i of "123"){
console.log(i) //1,2 3
}
但我們對普通對象進行for of
循環則遇到異常,需要我們自行添加。
Object.prototype[Symbol.iterator] = function {
var keys = Object.keys(this);
var index = 0;
return {
next: => {
var obj = {
value: this[keys[index]],
done: index+1 > keys.length
};
index++;
return obj;
}
};
};
var a = {
name:'ruby',
age:13,
home:"廣東"
}
for (var val of a) {
console.log(val);
}
Symbol.asyncIterator
Symbol.asyncIterator與for await of
循環一起使用,見上面異步一節。
Symbol.replace、search、split
這幾個靜態屬性都與正則有關,我們會發現這個方法名在字符串也有相同的臉孔,它們就是改變這些方法的行為,讓它們能接收一個對象,這些對象有相應的symbol保護方法。具體見下面例子:
class Search1 {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
console.log('foobar'.search(new Search1('bar')));
class Replace1 {
constructor(value) {
this.value = value;
}
[Symbol.replace](string) {
return `s/${string}/${this.value}/g`;
}
}
console.log('foo'.replace(new Replace1('bar')));
class Split1 {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
var index = string.indexOf(this.value);
return this.value + string.substr(0, index) + "/"
+ string.substr(index + this.value.length);
}
}
console.log('foobar'.split(new Split1('foo')));
Symbol.toStringTag
可以決定自定義類的 Object.prototype.toString.call的結果:
class ValidatorClass {
get [Symbol.toStringTag] {
return 'Validator';
}
}
console.log(Object.prototype.toString.call(new ValidatorClass));
// expected output: "[object Validator]"
此外,還有許多靜態屬性, 方便我們對語言的底層做更精致的制定,這里就不一一羅列了。
我們再看BigInt, 它就沒有這么復雜。早期JavaScript的整數范圍是2的53次方減一的正負數,如果超過這范圍,數值就不準確了。
console.log(1234567890123456789 * 123) //這顯然不對
因此我們非常需要這樣的數據類型,在它沒有出來前只能使用字符串來模擬。然后chrome67中,已經內置這種類型了。想使用它,可能直接在數字后加一個n,或者使用BigInt創建它。
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991);
// ? 9007199254740991n
const hugeString = BigInt("9007199254740991");
// ? 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff");
// ? 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");
console.log(typeof hugeBin) //bigint
反射機制的完善
反射機制指的是程序在運行時能夠獲取自身的信息。例如一個對象能夠在運行時知道自己哪些屬性被執行了什么操作。
最先映入我們眼簾的是IE8帶來的get, set關鍵字。這就是其他語言的setter, getter。看似是一個屬性,其實是兩個方法。
var inner = 0;
var obj = {
set a(val){
console.log("set a ")
inner = val
},
get a{
console.log("get a ")
return inner +2
}
}
console.log(obj)
obj.a = 111
console.log(obj.a) // 113
image_1dojfhdi1vqbdqg1hr4mkt52h9.png-11.9kB
但在babel.js還沒有誕生的年代,新語法是很難生存的,因此IE8又搞了兩個類似的API,用來定義setter, getter:Object.defineProperty與Object.defineProperties。后者是前者的強化版。
var inner = 0;
var obj = {}
Object.defineProperty(obj, 'a', {
set:function(val){
console.log("set a ")
inner = val
},
get: function{
console.log("get a ")
return inner +2
}
})
console.log(obj)
obj.a = 111
console.log(obj.a) // 113
而標準瀏覽器怎么辦?IE8時代,firefox一方也有相應的私有實現:__defineGetter__
,__defineSetter__
,它們是掛在對象的原型鏈上。
var inner = 0;
var obj = {}
obj.__defineSetter__("a", function(val){
console.log("set a ")
inner = val
})
obj.__defineGetter__("a", function{
console.log("get a ")
return inner + 4
})
console.log(obj)
obj.a = 111
console.log(obj.a) // 115
在三大框架沒有崛起之前,是MVVM的狂歡時代,avalon等框架就是使用這些方法實現了MVVM中的VM。
setter與getter是IE停滯十多年瀦中添加的一個重要特性,讓JavaScript變得現代化,也更加魔幻。
但它們只能監聽對象屬性的賦值取值,如果一個對象開始沒有定義,后來添加就監聽不到;我們刪除一個對象屬性也監聽不到;我們對數組push進一個元素也監聽不到,對某個類進行實例化也監聽不到……總之,局b限還是很大的。于是chrome某個版本添加了Object.observe,支持異步監聽對象的各種舉動(如"add", "update", "delete", "reconfigure", "setPrototype", "preventExtensions"),但是其他瀏覽器不支持,于是esma委員會又合計搞了另一個逆天的東西Proxy。
Proxy
這個是es6大名鼎鼎的魔術代理對象,與Object.defineProperty一樣,無法以舊有方法來模擬它。
下面是它的用法,其攔截器所代表的操作:
let p = new Proxy({}, {//攔截對象,上面有如下攔截器
get: function(target, name){
// obj.aaa
},
set: function(target, name, value){
// obj.aaa = bbb
},
construct: function(target, args) {
//new
},
apply: function(target, thisArg, args) {
//執行某個方法
},
defineProperty: function (target, name, descriptor) {
// Object.defineProperty
},
deleteProperty: function (target, name) {
//delete
},
has: function (target, name) {
// in
},
ownKeys: function (target, name) {
// Object.getOwnPropertyNames
// Object.getOwnPropertySymbols
// Object.keys Reflect.ownKeys
},
isExtensible: function(target) {
// Object.isExtensible。
},
preventExtensions: function(target) {
// Object.preventExtensions
},
getOwnPropertyDescriptor: function(target, prop) {
// Object.getOwnPropertyDescriptor
},
getPrototypeOf: function(target){
// Object.getPrototypeOf,
// Reflect.getPrototypeOf,
// __proto__
// Object.prototype.isPrototypeOf與instanceof
},
setPrototypeOf: function(target, prototype) {
// Object.setPrototypeOf.
}
});
這個對象在vue3, mobx中被大量使用。
Reflect
Reflect與Proxy一同推出,Reflect上的方法與Proxy的攔截器同名,用于一些Object.xxx操作與in, new , delete等關鍵字的操作(這時只是將它們變成函數方式)。換言之,Proxy是接活的,Reflect是干活的,火狐官網的示例也體現這一點。
var p = new Proxy({
a: 11
}, {
deleteProperty: function (target, name) {
console.log(arguments)
return Reflect.deleteProperty(target, name)
}
})
delete p.a
它們與Object.xxx最大的區別是,它們都有返回結果, 并且傳參錯誤不會報錯(如Object.defineProperty)。可能官方認為將這些元操作方法放到Object上有點不妥,于是推出了Reflect。
Reflect總共有13個靜態方法:
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
更順手的語法糖
除了添加這些方法外,JavaScript底層的parser也大動手術,讓它支持更多語法糖。語法糖都可以寫成對應的函數,但不方便??偟膩碚f,語法糖是想讓大家的代碼更加精簡。
新近添加如下語法糖:
對象簡寫,參看類的組織形式
擴展運算符(…
),用于對象的淺拷貝
箭頭函數,省略function關鍵字,與數學公式走近,能綁定this與略去return
for of(遍歷可迭代對象的所有值, for in是遍歷對象的鍵或索引)
數字格式化, 如1_222_333
字符串模板化與天然多行支持,如hello ${world}
冪運算符, **
可選鏈,let x = foo?.bar.baz;
空值合并運算符, let x = foo ?? bar;
函數的默認參數
總結
ECMAScript正在快速發展,經常會有新特性被引入,有興趣可以查詢babel的語法插件(https://www.babeljs.cn/docs/plugins),了解更詳細的用法。相信有了這些新特征的支持,大家再也不敢看小JavaScript了。
作者簡介:司徒正美,擁有十年純前端經驗,著有《JavaScript框架設計》一書,去哪兒網公共技術部前端架構師。愛好開源,擁有mass、Avalon、nanachi等前端框架。目前在主導公司的小程序、快應用的研發項目。
【END】
*請認真填寫需求信息,我們會在24小時內與您取得聯系。