1+1=10

扬长避短 vs 取长补短

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()可以直接调用:
function myAdd(a, b) {return a + b;}
let myAdd2 = (a, b) => a + b

console.log(myAdd.call({}, 1, 2))
console.log(myAdd2.call({}, 3, 4))
  • python的函数,和对象一样,可以直接用成员函数__call__()
def myAdd(a, b):
    return a + b

myAdd2 = lambda a, b: a + b

print(myAdd.__call__(1, 2))
print(myAdd.__call__(3, 4))
  • C++中,lambda表达式用法和普通对象一样,有成员函数operator()可以调用,但是函数指针没有这个待遇
#include <iostream>

int myAdd(int a, int b)
{
    return a + b;
}

auto myAdd2 = [](int a, int b) { return a + b; };

int main(int argc, char *argv[])
{
    std::cout << myAdd(1, 2) << std::endl; // no member function
    std::cout << myAdd2.operator()(3, 4) << std::endl;

    return 0;
}

编程范式(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)风格。

关于编程范式,脑袋比较大。相关名词听过很多,但似乎并没有太严格的定义。一种说法是,分成命令时和声明式两大类:

programming-paradims

从这个分类看,面向对象属于命令式,函数编程属于声明式。C语言属于命令式,SQL属于声明式的,而JavaScript、Python 和 C++,都支持多种范式。

声明式范式

关注“做什么”而不是“怎么做”,使用高阶函数和抽象的操作来表达代码的意图

const numbers = [1, 2, 3, 4, 5];

const sum = numbers.reduce((accumulator, currentValue) => {
  return accumulator + currentValue;
}, 0);

console.log(sum); // output 15

所谓高阶函数(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

命令式范式

使用循环和条件语句等明确的命令,按照步骤执行操作。

const numbers = [1, 2, 3, 4, 5];
let sum = 0;

for (let i = 0; i < numbers.length; i++) {
  sum += numbers[i];
}

console.log(sum); // output: 15

面向对象范式

将数据与操作封装在一个类中。

其实,前面声明式范式中用的例子,应该也是面向对象范式。因为:

  • numbers是个对象
  • 调用成员函数reduce 执行求和操作
const numbers = [1, 2, 3, 4, 5];

const sum = numbers.reduce((accumulator, currentValue) => {
  return accumulator + currentValue;
}, 0);

console.log(sum); // output 15

应用封装成类,会不好看,不过也可以:

class SumCalculator {
  constructor(numbers) {
    this.numbers = numbers;
  }

  calculateSum() {
    return this.numbers.reduce((accumulator, currentValue) => {
      return accumulator + currentValue;
    }, 0);
  }
}

const numbers = [1, 2, 3, 4, 5];

const sumCalculator = new SumCalculator(numbers);
const sum = sumCalculator.calculateSum();

console.log(sum); // output 15

ECMAScript6之前的版本,不支持class,改一下也还行:

function SumCalculator(numbers) {
  this.numbers = numbers;
}

SumCalculator.prototype.calculateSum = function() {
  return this.numbers.reduce(function(accumulator, currentValue) {
    return accumulator + currentValue;
  }, 0);
};

var numbers = [1, 2, 3, 4, 5];
var sumCalculator = new SumCalculator(numbers);
var sum = sumCalculator.calculateSum();

console.log(sum); // output 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:

o = 123

// standard way to get the prototype of an object
console.log(Object.getPrototypeOf(o) === Number.prototype) // true
console.log(Object.getPrototypeOf(Number.prototype) === Object.prototype) // true

// another way to get the prototype of an object
console.log(o['__proto__'] === Number.prototype) // true
console.log(o['__proto__']['__proto__'] === Object.prototype) // true

// non-standard way to get the prototype of an object
console.log(o.__proto__ === Number.prototype) // true
console.log(o.__proto__.__proto__ === Object.prototype) // true

注意:__proto__ 是非标准属性。标准的方式是使用 Object.getPrototypeOf() 方法。

指定 prototype

数据成员和方法都可以继承,创建对象时,可以通过__proto__直接指定(这个方法是标准的):

p = {hello: "1+1=10", world: "1+1=2", speak: ()=>"hello 1+1=10"}
c = {__proto__: p, world: "1+1=3"}

console.log(c.hello, c.world, c.speak()) // 1+1=10 1+1=3 hello 1+1=10

也可以指定多级

c = {__proto__: {
      hello: "1+1=10",
      world: "1+1=2",
    __proto__: {speak: ()=>"hello 1+1=10"}
    },
     world: "1+1=3"}

console.log(c.hello, c.world, c.speak()) // 1+1=10 1+1=3 hello 1+1=10

或者创建完再指定:

p = {hello: "1+1=10", world: "1+1=2", speak: ()=>"hello 1+1=10"}
c = {world: "1+1=3"}

Object.setPrototypeOf(c, p)
// c.__proto__ = p

console.log(c.hello, c.world, c.speak()) // 1+1=10 1+1=3 hello 1+1=10

也可以使用Object.create():

p = {hello: "1+1=10", world: "1+1=2", speak: ()=>"hello 1+1=10"}
c = Object.create(p, {world: {value: "1+1=3"}})

console.log(c.hello, c.world, c.speak()) // 1+1=10 1+1=3 hello 1+1=10

构造函数

与前面__proto__不同,构造函数还有一个prototype。这个对应该函数创建对象的__proto__

比如,定义一个类Person,它有一个数据成员,和一个成员函数:

function Person(name) {
    this.name = name;
}

Person.prototype.greet = function() {
    console.log(`Hello ${this.name}!`);
};

const person1 = new Person("1+1=10");
person1.greet();

注意:

console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(person1.__proto__ === Person.prototype); // true

上的类定义方式等同于ES6的class写法:

class Person {
    constructor(name) {
        this.name = name;
    }
    greet() {
        console.log(`Hello ${this.name}!`);
    }
}

const person1 = new Person("1+1=10");
person1.greet();

类继承

使用class:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    speak() {
        console.log(`${this.name} barks.`);
    }
}

const dog = new Dog("Buddy", "Labrador");
dog.speak(); // Output: Buddy barks.

其底层仍然是原型:

console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

使用原型进行改写:

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
    console.log(`${this.name} barks.`);
};

const dog = new Dog("Buddy", "Labrador");
dog.speak(); // Output: Buddy barks.

单线程

JavaScript最初是作为浏览器端脚本语言而设计的。在Web浏览器中,用户的交互和浏览器的事件驱动是主要的操作,因此单线程模型足够满足这种需求。通过将异步任务委托给事件队列,JavaScript能够在用户交互时响应事件,而不会阻塞主线程。

Web浏览器中的DOM(文档对象模型)是单线程的,多个线程同时修改DOM可能导致不一致和不可预测的结果。JavaScript使用单线程来保证对DOM的操作是线程安全的。

看个例子:

  • setTimeout 函数,并设置了一个零毫秒的定时器。回调函数并不会立即执行;而是被安排在事件循环的下一次迭代中执行。

  • Promise.resolve().then(...) 语句。then 回调似乎也被安排在事件循环的下一次迭代中执行,实际上和事件循环没有关系,它在进入事件循环下次迭代前已经执行完毕。

console.log("Start");

setTimeout(() => {
    console.log("Timeout callback");
}, 0);

Promise.resolve().then(() => {
    console.log("Promise callback");
});

console.log("End");

结果:

Start
End
Promise callback
Timeout callback

注意:setTimeout()不是ECMAScript规范的组成部分,尽管浏览器和Node.js都支持它,Qt中的QJSEngine不支持它。尽管QJSEngine没有事件循环,但这不影响QJSEngine实现Promise。

也可以使用 async、await 语法:

console.log("Start");

(async () => {
    await new Promise(resolve => setTimeout(resolve, 0));
    console.log("Timeout callback");
})();

(async () => {
    await Promise.resolve();
    console.log("Promise callback");
})();

console.log("End");

另外: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

Comments