WebGL Tutorial
and more

JavaScript概述

撰写时间:2024-02-11

修订时间:2024-04-10

概述

从TIOBE的排行榜来看,从2004年开始,JavaScript一直排在所有编程语言的前10名。2004至2014年排在第9名,并于2014年被评为当年之最受欢迎的语言(language of the year)。2019年上升至第8名,2024年上升至第6名。

目前,排在前5名的编程语言分别为:Python, C, C++, Java, 以及C#。

随着更多Web标准的出现,JavaScript的重要性将愈发突显。

C语言高效而强大,但它一直缺乏一个简单而易用的图形库。而有Canvas支持的JavaScript则无此忧。随着ArrayBuffer的加入,不仅WebGL从此可在网页上实现,更被WebAssembly充分利用而实现了桌面应用程序到Web应用的完美移植。而2021年开始的WebGPU技术已让我们感到了不可思议。Web AI?它已经悄悄到来了。

世界变化速度开始比过山车还要更猛了。而所有的这一切,都有JavaScript在背后默默地强劲支撑。更不用说,JavaScript是Web网络的原生土著语言。

过去,JavaScript被认为只不过是一种脚本语言。但现在,越来越多的人认为,JavaScript是一种具备操控网页元素的脚本能力的编程语言。

过去,JavaScript被认为在学习时还在睡觉;而现在,JavaScript被认为在睡觉时仍在学习。

经过较长时间的发展、演化及规范化,JavaScript有许多实用的、独具一格的、令人着迷的编程语言特性。下面开始有趣的JavaScript特性之旅。

数据类型

变量没有固定的数据类型

有固定的数据类型,但声明变量时,变量没有固定的数据类型。

let a = 5; // number a = "Hello"; // string

数据类型自动转换

当数值类型与字符串类型相加时,得到一个字符串的结果。

let a = 5; a = 5 + "Hello"; // string

字符串的构建

可使用Unicode来创建字符串:

let footBall = '\u26BD'; console.log(footBall); // ⚽

可通过本站制作的HTML Unicode工具来获取更多的Unicode符号。

还可转换Unicode的码位,得到相应的十进制数字,然后可在HTML中使用,如:在HTML中显示⚽

要构建一个字符串,不管是否多行,还是需要引用其他变量,都无比轻松:

let num = 15; let str = ` He gave me ${num} apples. And eventually additional 5 more. `;

数值的自由表达与转换

字面符

对于十进制的27,我们可以在字面符中以不同的进制来指定:

二进制。以0b0B为前缀:

var num = 0b00011011; // 27 // or var num = 0B00011011; // 27

八进制:以0o0O为前缀:

var num = 0o33; // 27 // or var num = 0O33; // 27

十六进制。以0x0X为前缀:

var num = 0x1B; // 27 // or var num = 0X1B; // 27

对于数据类型为BigInt的数值,则在上面各种进制的表达式之后加上n

var num = 12345678987654321n; var num = 0B1011100111110011n; var num = 0o123456712345670n; var num = 0XFF33CC8899AA66DDn;

对于较大的数字,可以加入下划线_来分隔,以增加可读性:

var num = 12_345_678_987_654_321n; var num = 9_8765_4321n; // chinese convention,9亿8765万4321

转化为不同进制的字符串

let num = 27; console.log(num.toString(2)); // 11011 console.log(num.toString(8)); // 33 console.log(num.toString(16)); // 1b

自由的对象创建方式

创建一个对象无比的简单。

let obj = { name: 'mike', age: 25 };

对象在创建后,可以自由地添加或删除属性。

obj.email = '123@263.com'; delete obj.age;

最终结果:

console.log(obj); /* { name: 'mike', email: '123@263.com'; } */

遍历对象属性名值:

let entries = Object.entries(obj); for (const [key, value] of entries) { console.log(key); console.log(value); }

指针或引用

JavaScript也存在指针的操作。当变量引用Object类型时,即为引用关系。

let a = { name: 'Sarkuya', age: 25 }; let b = a; a.name = 'Mike'; console.log(b); // {name: "Mike", age: 25}

变量ab均指向同一个对象,当a改变时,b的值也同时改变。

数组

数组的创建

创建数组很直观。

let arr = [1, 2, 3, 4, 5];

数组是栈

数组是以栈的形式来组织的。

let arr = []; arr.push(1); arr.push(2); console.log(arr); // [1, 2] arr.pop(); console.log(arr); // [1]

栈的特点是先进后出(FILO,first in, last out)。上例中,数值1为栈底,数值2为栈顶。Array类的push方法进栈,pop方法出栈。

可调用shift方法将栈底元素,也即第0个数组元素移出栈。

let arr = [1, 2, 3]; let bottom = arr.shift(); console.log(bottom); // 1 console.log(arr); // [2, 3]

数组的合并

concat方法可将多个数组或元素合并进一个数组中。

let arr1 = [1, 2, 3]; let arr2 = [4, 5]; let num = 6; let arr3 = arr1.concat(arr2, num); console.log(arr3); // [1, 2, 3, 4, 5, 6]

使用展开操作符...,也可以字面符的方式来合并数组元素。

let arr1 = [3, 4, 5]; let arr2 = [1, 2, ...arr1, 6, 7]; console.log(arr2); // [1, 2, 3, 4, 5, 6, 7]

上面如果不使用展开操作符...操作符,则数组元素为数组。

let arr1 = [3, 4, 5]; let arr2 = [1, 2, arr1, 6, 7]; console.log(arr2); // [1, 2, [3, 4, 5], 6, 7]

高效的代码

无需借助第三方变量,我们可以一行代码就实现两个变量交换数值。

let a = 10; let b = 20; [b, a] = [a, b]; console.log(a, b); // 20, 10

先将ab置于一个数组,再通过数组解包,直接分别赋值于ba,从而高效地进行数值交换。

函数

自调用函数

<script type="module"> (() => { let a = 5; console.log(a); })(); </script>

上面的代码,结合了JavaScript模块、lambda表达式及自调用函数的特点,使相关代码得以在页面内容加载完毕后自动运行。相当于:

<script> function init() { let a = 5; console.log(a); } window.addEventListener('DOMContentLoaded', init); </script>

内嵌函数

内嵌函数是JavaScript的一大特色。

function aFunc() { let a = sum(3, 5); console.log(a); function sum(a, b) { return a + b; } }

如果某函数只在一个地方使用,而又不想与其他局部变量相互影响,则可像上面一样创建一个内嵌函数。它的好处是,对外部不可见,而又与内部的其他变量相互隔离。

因为是内部使用,甚至可以无需函数名而改写为自调用匿名函数:

function aFunc() { let x = ((a, b) => a + b)(3, 5); let m = 5, n = 10; let y = (() => { let temp = m * 2 + n; temp++; return temp; })(); console.log(x); // 8 console.log(y); // 21 }

这种方式,除具备上面两个特点外,还有另外一个特点:它可访问闭包范围外的变量诸如m。利用这种灵活性,我们可节省多少的函数参数传导?

函数指针

JavaScript支持函数指针。

function normalFunc() { console.log('This is a function.'); } function invoker(func) { func(); } invoker(normalFunc);

invoker的参数是一个函数指针,在其方法体中,直接调用了作为参数所传进来的函数。

回调函数

更进一步,编制非常实用的回调机制。其基本形式如下:

function dataConsumer(a, b) { let c = a + b; console.log(c); } function dataProvider(callbackFunc) { let a = 5; let b = 10; callbackFunc(a, b); } dataProvider(dataConsumer);

回调函数的本质,是由一个数据提供者dataProvider,负责调用作为参数所传进的函数dataConsumer,并且将自己所持有的数据作为函数参数传递给后者。意为,我负责提供数据,并负责回调你以传送相应的数据。

ArrayforEach方法就是基于上面的原理而编写的回调函数:

let arr = [5, 10, 15]; arr.forEach((element, index) => { console.log(index + ': ' + element); });

forEach方法就类似于上面的dataProvider函数,以参数elementindex回调了一个匿名函数。然后,在该匿名函数中,我们就可随意使用所传入的参数。

上面的回调函数是单程的,即dataConsumer函数处置完数据后就结束了。

更进一步,我们可以利用回调函数的返回值,实现数据提供者也数据使用者之间的双程沟通。

function dataCustomize(config) { config.fontSize = '20'; return config; } function dataProvider(callbackFunc) { let config = {}; let newConfig = callbackFunc(config); setFontSize(newConfig.fontSize); } function setFontSize() { // ... } dataProvider(dataCustomize);

dataProvider函数先初始化一个config变量,并通过回调机制传给dataCustomize函数由其负责具体设置该对象的值。之后,dataCustomize函数返回设置之后的值,dataProvider接收此返回值后,又进一步根据此值来设置相应的字体大小。这就实现了双程沟通。

这种设计机制有许多好处。dataProvider函数负责config的生命周期,但必须考虑将设置数值的自由还给客户端。而客户端dataCustomize函数只需根据实际需求,专注于设置相应的数值就行了,无需考虑过多的细节。双方合作得都很愉快。

Array类提供了许多这样的双程回调函数。

let arr = [20, -5, 3, -20]; let newArr = arr.filter(element => element < 0); console.log(newArr); // [-5, -20]

filter方法用于筛选出数组中符合特定条件的元素。而筛选条件由回调匿名函数来定义。而对于filter所传进的任一元素,匿名回调函数将条件设定为:如果其值小于0,则筛选出来。filter方法根据各个元素的返回值重新生成了一个新的数组并赋值于变量newArr

这就是回调机制的强大之处,filter方法将重新生成新数组的算法隐藏起来了,但又可让客户端自由地指定各种各样的筛选条件,从而实现了固定算法与灵活配置的完美结合。

除了上面的filter方法之外,Array类的some, every, map, reduce等方法均属于这种情况。

Lambda表达式

Lambda表达式是匿名函数的一种快捷表现方式:

let sum = (a, b) => { return a + b; };

相当于:

function sum(a, b) { return a + b; }

Lambda表达式在函数参数为函数时经常用到:

let arr = [1, 2, 3]; let newArr = arr.map((item, idx) => { return `${idx}: ${item}`; });

如果Lambda表达式的参数只有1个,可省略参数列表的括号()

let newArr = arr.map(item => { return item * 2; });

如果Lambda表达式的函数体内,只有1条返回语句,则可省略大括号{}以及return关键字:

let newArr = arr.map(item => item * 2);

而如果需要将参数直接传至其他方法或函数处理,则可更加简练,直接编写该方法或函数的名称即可:

let arr = [1, 2, 3]; arr.forEach(console.log);

相当于:

let arr = [1, 2, 3]; arr.forEach((item, idx, array) => { console.log(item, idx, array); });

结果输出:

1 - 0 - [1, 2, 3] 2 - 1 - [1, 2, 3] 3 - 2 - [1, 2, 3]

注意,因为consolelog方法的参数为可变长参数,因此forEach的回调函数的3个参数全部都自动传送过去了。

如果我们只需使用其中的某个参数,可编写自定义函数:

let arr = [1, 2, 3]; function show(item) { console.log(item); } arr.forEach(show); // 1, 2, 3

自定义函数show只接受1个参数,因此回调函数的后两个参数被自动忽略了。

因此,当lambda表达式浓缩为只有方法或函数名称时,需同时考虑回调函数及所调用方法、函数的参数数量、顺序,否则可能出现不易觉察的bug。

构造器

function Rect(x, y, w, h) { this.x = x; this.y = y; this.width = w; this.height = h; this.offset = (xOffset, yOffset) => { this.x += xOffset; this.y += yOffset; }; } let rect = new Rect(10, 10, 50, 50); rect.offset(20, 20);

ECMA 6class出来之前,这是JavaScript最典型的创建类的对象的方式。Rect称为构造函数,也称为对象rect的构造器。我们可以用下面代码查看该构造器:

console.log(rect.constructor); /* * function Rect(x, y, w, h) { * ... * } */

作为对比,也可像下面一样创建实例:

let rect = {}; rect.init = function(x, y, w, h) { this.x = x; this.y = y; this.width = w; this.height = h; }; rect.offset = function(xOffset, yOffset) { this.x += xOffset; this.y += yOffset; }; rect.init(10, 10, 50, 50); rect.offset(20, 20);

注意,作为创建对象的用例,上面在为rect声明方法时,不能使用lambda的形式。

在一些图形应用中,我很喜欢使用下面便捷的伪构造器代码:

function Rect(x, y, width, height) { return {x:x, y:y, width:width, height:height}; } let rect = Rect(10, 10, 50, 50);

Rect虽不是构造器,却返回一个通过字面符构造器来创建的对象实例。

自动运行代码

let a = 5; let b = 3; let code = `console.log(${a} + ${b});`; Function(code)(); // 8

Function(code)()效果等同与eval(code),但比后者更安全、高效。具体参见函数类一节。

解包

参见在WebGL入门教程中的JavaScript解包

参考资源

  1. TIOBE Index
  2. ECMA 262