接前面
- JavaScript引擎与运行时入门小记:在哪些环境下可以运行JavaScript
从头看看 JavaScript
JavaScript
对于JavaScript,Mozilla文档这么描述:
JavaScript(JS)是一种轻量级的解释型(或即时编译)编程语言,具有一等函数(first-class function)。虽然它最出名的是网页脚本语言,但许多非浏览器环境也使用它,如Node.js、Apache CouchDB和Adobe Acrobat。
JavaScript是一种基于原型的(prototype-based)、多范式的(multi-paradigm)、单线程的、动态的语言,支持面向对象、命令式(imperative)和声明式(delarative)风格。
里面似乎提及好多东西:
- 一等函数(first-class function)
- 三种编程范式:面向对象,命令式,声明式
- 基于原型(prototype-based)
- 单线程
接下来,先准备环境,再逐一看一看:
准备环境
- 在Firefox浏览器下:Ctrl+Shift+K 直接调出 console 界面内,输入javascript代码,按 Ctrl + Enter 执行。
- Nodejs命令行下:直接 node xxx.js 执行
- Visual Studio Code + Code Runner + Node.js下:创建 xxx.js文件,点击运行。(运行前记得save文件!!!)
一等函数(first-class function)
Mozilla中说的:
A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.
不过,这段话,我看完和没看一样,看不懂。函数在JavaScript中是一等公民,在Python中也是。那么在C++中是不是,在C中是不是??
个人感觉上:C函数指针基本满足要求,C++中匿名函数和闭包满足这个更没问题。
但是,它们和JavaScript以及Python中的函数确实是有差异的,或许,可以举例对比一下??
- javascript中的函数,和其他对象一样,有成员函数
call()
可以直接调用:
1 2 3 4 5 |
|
- python的函数,和对象一样,可以直接用成员函数
__call__()
:
1 2 3 4 5 6 7 |
|
- C++中,lambda表达式用法和普通对象一样,有成员函数
operator()
可以调用,但是函数指针没有这个待遇
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
编程范式(programming paradigm)
Paradigm 读音 /ˈpærədaɪm/,源于古希腊语。同样gm组合中g不发音的单词还有“phlegm” 和 "diaphragm"。
对于JavaScript,Mozilla文档文档中提到支持三种风格:
JavaScript(JS)是一种轻量级的解释型(或即时编译)编程语言,具有一级函数(first-class function)。虽然它最出名的是网页脚本语言,但许多非浏览器环境也使用它,如Node.js、Apache CouchDB和Adobe Acrobat。
JavaScript是一种基于原型的(prototype-based)、多范式的(multi-paradigm)、单线程的、动态的语言,支持面向对象、命令式(imperative)和声明式(delarative)风格。
关于编程范式,脑袋比较大。相关名词听过很多,但似乎并没有太严格的定义。一种说法是,分成命令时和声明式两大类:
从这个分类看,面向对象属于命令式,函数编程属于声明式。C语言属于命令式,SQL属于声明式的,而JavaScript、Python 和 C++,都支持多种范式。
声明式范式
关注“做什么”而不是“怎么做”,使用高阶函数和抽象的操作来表达代码的意图
1 2 3 4 5 6 7 |
|
所谓高阶函数(Higher-order function),缩写HOF,是指满足下面至少一项的函数:
- 接受一个或多个函数作为参数
- 将一个函数作为返回值返回
例子中reduce是一个高阶函数,另外,
JavaScript的Array有如下几个高阶函数:
- map():依次处理每个元素,返回一个新Array。新Array可看作是原Array的映射(map)。
- forEach():遍历每个元素
- reduce():计算累计值
- filter():筛选符合条件的元素,组成 新Array
这几个函数都接受一个处理函数:处理函数都 接受当前元素、当前元素索引、当前元素所属数组,另外reduce对应的处理函数还要接受当前累计值。
Python中有类似的函数:
- map()
- filter()
- functools.reduce()
但是Python中的生成器表达式和列表推导式似乎更受欢迎。
C++标准库有:
- std::transform
std::remove_if
- std::accumulate
以及C++17引入支持并发的
- std::reduce
std::transform_reduce
命令式范式
使用循环和条件语句等明确的命令,按照步骤执行操作。
1 2 3 4 5 6 7 8 |
|
面向对象范式
将数据与操作封装在一个类中。
其实,前面声明式范式
中用的例子,应该也是面向对象范式。因为:
- numbers是个对象
- 调用成员函数reduce 执行求和操作
1 2 3 4 5 6 7 |
|
应用封装成类,会不好看,不过也可以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
ECMAScript6之前的版本,不支持class,改一下也还行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
基于原型(prototype-based)
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
JavaScript使用原型继承的机制来实现对象之间的继承关系。对象是包含属性的动态包,而且每个对象都有一个可以包含属性和方法原型对象。当访问一个对象的属性或方法时,如果该对象本身没有定义这个属性或方法,JavaScript会去查找它的原型对象,以此类推,直到找到该属性或方法或者达到原型链的顶端。
访问 prototype
JavaScript中的几乎所有对象(除了一些特殊对象,如 Object.prototype
本身)都可以通过 __proto__
属性访问其原型对象。这使得对象之间可以通过原型链共享属性和方法。
比如数字 123,其原型是 Number.prototype,原型的原型是 Object.prototype:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
注意:
__proto__
是非标准属性。标准的方式是使用Object.getPrototypeOf()
方法。
指定 prototype
数据成员和方法都可以继承,创建对象时,可以通过__proto__
直接指定(这个方法是标准的):
1 2 3 4 |
|
也可以指定多级
1 2 3 4 5 6 7 8 |
|
或者创建完再指定:
1 2 3 4 5 6 7 |
|
也可以使用Object.create():
1 2 3 4 |
|
构造函数
与前面__proto__
不同,构造函数还有一个prototype
。这个对应该函数创建对象的__proto__
比如,定义一个类Person,它有一个数据成员,和一个成员函数:
1 2 3 4 5 6 7 8 9 10 |
|
注意:
1 2 |
|
上的类定义方式等同于ES6的class写法:
1 2 3 4 5 6 7 8 9 10 11 |
|
类继承
使用class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
其底层仍然是原型:
1 2 |
|
使用原型进行改写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
单线程
JavaScript最初是作为浏览器端脚本语言而设计的。在Web浏览器中,用户的交互和浏览器的事件驱动是主要的操作,因此单线程模型足够满足这种需求。通过将异步任务委托给事件队列,JavaScript能够在用户交互时响应事件,而不会阻塞主线程。
Web浏览器中的DOM(文档对象模型)是单线程的,多个线程同时修改DOM可能导致不一致和不可预测的结果。JavaScript使用单线程来保证对DOM的操作是线程安全的。
看个例子:
setTimeout
函数,并设置了一个零毫秒的定时器。回调函数并不会立即执行;而是被安排在事件循环的下一次迭代中执行。
Promise.resolve().then(...)
语句。then
回调似乎也被安排在事件循环的下一次迭代中执行,实际上和事件循环没有关系,它在进入事件循环下次迭代前已经执行完毕。
1 2 3 4 5 6 7 8 9 10 11 |
|
结果:
1 2 3 4 |
|
注意:setTimeout()不是ECMAScript规范的组成部分,尽管浏览器和Node.js都支持它,Qt中的QJSEngine不支持它。尽管QJSEngine没有事件循环,但这不影响QJSEngine实现Promise。
也可以使用 async、await 语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
另外:HTML5 提出了 Web Worker 标准,Node.js 提供了 worker_threads 模块,允许创建多个线程,但是这些都没改变 JavaScript 单线程的本质。
参考
- https://developer.mozilla.org/en-US/docs/Web/JavaScript
- https://en.wikipedia.org/wiki/Comparison_of_programming_paradigms
- https://www.educative.io/blog/declarative-vs-imperative-programming
- https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function
- https://en.wikipedia.org/wiki/First-class_function