1+1=10

扬长避短 vs 取长补短

JavaScript学习笔记(二)

继续梳理JavaScript基本概念。

有两个东西,表述起来像绕口令:

  • global object:全局对象。全局环境的顶层对象。JavaScript中,始终会定义一个全局对象。(globalThis)
  • global objects:全局对象们。涵盖了所有在全局环境中可用的内置对象。(standard built-in objects)。

全局对象

在 JavaScript 中,总是会存在一个全局对象。 在 Web 浏览器中,当使用使用 var 关键字定义全局变量时,它们将被创建为该全局对象的成员(在 Node.js 中,情况有所不同)。 例如:

  • 在 Web 浏览器中,绝大多数 JavaScript 代码不作为后台任务使用。它们将 Window 作为其全局对象。
  • 后台 Worker 中运行的代码有一个 WorkerGlobalScope 对象作为其全局对象。
  • 在 Node.js 下运行的脚本有一个名为 global 的对象作为其全局对象。

先看个例子(在firefox、chrome、edge下可以运行,在node.js下不适用):

function myAdd(a, b) {
    return a + b
}

var myAdd2 = function (a, b) {
    return a + b
}

let myAdd3 = (a, b) => a + b

globalThis.myAdd4 = (a, b) => a + b

console.log(myAdd(1, 2), myAdd2(1, 2), myAdd3(1, 2), myAdd4(1, 2))
console.log(globalThis.myAdd(1, 2), globalThis.myAdd2(1, 2), globalThis.myAdd4(1, 2))

注意:

  • globalThis 的属性自动是 全局变量(例子中myAdd4)。
  • 通过var定义的变量,都会作为 globalThis 的属性存在(注意myAdd和myAdd2)。
  • 这个也仅适用于浏览器,nodejs行为不一样,见后面var部分。

globalThis

globalThis 是在ES11(ES2020)中引入的,用于访问 全局对象。在Qt中,截至6.5,QJSEngine尚不支持这个东西。

在globalThis引入之前,

  • 浏览器中 使用 window、self、frames 指代全局对象。Web workers中使用 self 指代全局对象
  • Node.js中使用 global 指代全局对象

全局对象,有属性指向它自己,比如

在浏览器下:

console.log(globalThis === globalThis.globalThis); // true
console.log(window === window.window); // true
console.log(self === self.self); // true
console.log(frames === frames.frames); // true

在Nodejs下:

console.log(globalThis === globalThis.globalThis); // true
console.log(global === globalThis.global); // true

在QJSEngine下:

在QJSEngine(Qt6.6)下,不支持globalThis,也没有其他self、window、global等变通方式。

但是我可以通过让函数返回this来间接获取它:

#include <QCoreApplication>
#include <QJSEngine>

const char *js = R"js(
var a = "1+1=10";

let myGlobal = (function(){return this})();
console.log(myGlobal.Math === Math);
console.log(myGlobal.app === app);
console.log(myGlobal.a === a);

console.log(this.Math === Math);
console.log(this.a === a);
)js";

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QJSEngine engine;
    engine.installExtensions(QJSEngine::ConsoleExtension);
    engine.setObjectOwnership(&app, QJSEngine::CppOwnership);
    engine.globalObject().setProperty("app", engine.newQObject(&app));

    engine.evaluate(js);

    return 0;
}

输出结果

js: true
js: true
js: true
js: true
js: true

var 与全局对象

Nodejs手册提到:

In browsers, the top-level scope has traditionally been the global scope. This means that var something will define a new global variable, except within ECMAScript modules. In Node.js, this is different. The top-level scope is not the global scope; var something inside a Node.js module will be local to that module, regardless of whether it is a CommonJS module or an ECMAScript module.

在浏览器中,顶级作用域传统上就是全局作用域,这意味着var会定义全局变量。而在Node.js中,顶级作用域不是全局作用域,var定义的变量属于当前模块。

node.js

console.log(this === globalThis) // false

QJSEngine

在Qt下,即使没有globalThis,但是我们也可以在C++端轻松验证——var定义的变量是globalObject的属性:

QJSEngine engine;
engine.evaluate(R"(var a = "1+1=10")");

qDebug() << engine.globalObject().property("a").toString(); // 1+1=10

this 用法

不同于C++中的this和python中的self,javascript中这个东西似乎很乱。MDN中说:在非严格模式下,this 总是指向一个对象,在严格模式下可以是任意值。

this的值取决于函数的调用方式和上下文环境,状况很复杂。简单几个情况如下:

  • 在对象的方法中,this指向调用该方法的对象
  • 在构造函数中,this指向正在构造的对象
  • 类静态方法中,this指向类
  • 在箭头函数中,this指向定义箭头函数时的外层对象,不会随着调用方式改变
  • 在事件处理函数中,this指向触发事件的元素??
  • 在call,apply或bind方法中,this可以被显式地绑定到任意对象
  • 在全局作用域或普通函数中,this不一定指向全局对象

对象方法

和C++、Python有点类似。这个比较直观,this指向调用该方法的对象:

const test = {
    name: '1+1=10',
    func: function () {
      return this.name;
    },
};

console.log(test.func()); // 1+1=10

const test2 = {
    name: '1+1=2',
};
test2.func = test.func;
console.log(test2.func()); // 1+1=2

在函数内部,this 的值取决于函数如何被调用。同一个函数,根据其调用的方式,this 的值是不同的:

function func() {
    return this.name
}

const obj1 = {name: 'obj1', func: func}
const obj2 = {name: 'obj2', func: func}

console.log(obj1.func()) // obj1
console.log(obj2.func()) // obj2

这这个基础上,对原型使用,也是符合预期:

const obj3 = {name: 'obj3', __proto__: obj1}
console.log(obj3.func()) // obj3

构造函数

这个也容易理解。当一个函数被用作构造函数(使用 new关键字)时,无论构造函数是在哪个对象上被访问的,其 this 都会被绑定到正在构造的新对象上。

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

const person1 = new Person('1+1=10', 41);

console.log(person1.name); // 1+1=10
console.log(person1.age); // 41

类构造函数与此一样,this指向类实例:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

const person1 = new Person('1+1=10', 41);

console.log(person1.name); // 1+1=10
console.log(person1.age); // 41

类静态方法

这个和python也有些类似

class MyClass {
    static staticMethod() {
        return this;
    }
}

console.log(MyClass.staticMethod()); // class MyClass

箭头函数

在箭头函数中,this 保留了上下文的 this 值。

这个东西,挺反知觉的:

const obj = {
    name: '1+1=10',
    sayHello: () => {
        console.log(`Hello, ${this.name}!`);
    }
};

obj.sayHello();

输出是

Hello, undefined!

而不是

Hello, 1+1=10!

需要这样才行(注意:它指向全局的this,同时注意,在nodejs下this不等同于globalThis):

const obj = {
    name: '1+1=10',
    sayHello: () => {
        console.log(`Hello, ${this.name}!`);
    }
};

this.name = '1+1=2';

obj.sayHello(); // 1+1=2

在下面情况中,箭头函数中的this指向obj:

var obj = {
    name: 'obj',
    say: function() {
      console.log(this.name);
      let f = () => console.log(this.name); 
      return f;
    }
  };

  obj.say()(); // obj obj

事件处理函数

如下程序,在浏览器下显示”true“,即this指向globalThis:

<!DOCTYPE html>
<html>
<head>
    <title>DOM Event Handler</title>
</head>
<body>
    <button onclick="handleClick()">Click me</button>

    <script>
        function handleClick() {
            alert(this === globalThis);
        }
    </script>
</body>
</html>

而下面addEventListenser用法,结果显示"Button id is myButton",即this指向button:

<!DOCTYPE html>
<html>
<head>
    <title>Button Event Listener Demo</title>
</head>
<body>
    <button id="myButton">Click Me</button>

    <script>
        document.getElementById("myButton").addEventListener("click", function() {
            alert("Button id is: " + this.id);
        });
    </script>
</body>
</html>

call、apply、bind

JavaScript中的call()和apply()方法都是用来改变函数的this指向的,它们可以让一个函数以指定的对象作为执行上下文,从而访问该对象的属性和方法。

const person = {
    name: '1+1=10',
    age: 100,
};

// Function to display person's details
function displayDetails() {
    console.log(`Name: ${this.name}, Age: ${this.age}`);
}

// Using call
displayDetails.call(person);

// Using apply
displayDetails.apply(person);

// Using bind
const boundFunction = displayDetails.bind(person);
boundFunction();

全局作用域或普通函数

在全局作用域或普通函数中:非严格模式下,this指向全局对象(globalThis);严格模式下,this是undefined??

全局作用域中 this

全局作用域下,this并不一定是全局对象:

先看一段代码:

// 'use strict';
console.log(this.Math === Math);

在Firefox浏览器与QJSEngine下,结果都是true,在Node.js中,结果为false。是否是strict没有影响。

更直接一点:

console.log(this === globalThis);

在Firefox下true,在node.js下为false,在QJSEngine下报错(不支持globalThis)。

普通函数中的this

搞不懂,普通函数中为什么要设计this。

在Firefox、Node.js、QJSEngine下测试以下:

function returnThis() {
    return this;
}

console.log(typeof returnThis());
console.log(returnThis().Math === Math);

结果:

object
true

在严格模式下测试:

'use strict';
function returnThis() {
    return this;
}

console.log(typeof returnThis());

结果

undefined

全局对象们(内置标准对象)

JavaScript内置标准对象是指JavaScript语言自带的一些对象,它们可以直接在代码中使用,而不需要引入其他文件或库。

这东西挺多,暂时先摘抄一点。

JavaScript内置标准对象可以分为以下几类:

值属性

这些全局属性返回一个简单值,这些值没有自己的属性和方法。

  • globalThis
  • Infinity
  • NaN
  • undefined

关于NaN一个梗,香蕉是怎么练成的:

console.log(('b'+ 'a' + + 'b' + 'a').toLowerCase()) // banana

注,NaN的特点:

console.log(typeof NaN) // number
console.log(NaN === NaN) // false

对于C++看看:

// c++
double a = std::nan("");
std::cout << std::boolalpha << (a == a) << std::endl; // false

函数属性

这些全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。

例如:

  • eval(),
  • isFinite(),
  • isNaN(),
  • parseFloat(),
  • parseInt()

基本对象

这些对象是定义或使用其他对象的基础。

例如:

  • Object,
  • Function,
  • Boolean,
  • Symbol

注意:symbol 是一种基本数据类型,每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。

const mySymbol = Symbol('mySymbol');

const obj = {
    normal: 'Hello, normal',
    [mySymbol]: 'Hello, symbol!'
};

console.log(obj[mySymbol]); // Hello, symbol!

symbol 在 for in中不可见,在JSON.stringify()也不可见:

const mySymbol = Symbol('mySymbol');

const obj = {
    normal: 'Hello, normal',
    [mySymbol]: 'Hello, symbol!'
};

for (let key in obj) {
    console.log(key); // Hello, normal
}

console.log(JSON.stringify(obj)); // {"normal":"Hello, normal"}

错误对象

这些对象是一种特殊的基本对象,它们用来表示和处理错误。

例如

  • Error
  • EvalError
  • RangeError
  • ReferenceError

数字和日期对象

这些对象用来表示和操作数字、日期和执行数学计算的对象。

例如

  • Number
  • BigInt
  • Math
  • Date

字符串对象

这些对象用来表示和操作字符串的对象。例如,String,RegExp等。

可索引的集合对象

这些对象用来表示和操作按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。

例如

  • Array,
  • Int8Array,
  • Uint8Array,
  • Uint8ClampedArray

使用键的集合对象

这些对象用来表示和操作使用键来存储数据的集合,包括可迭代的Map和Set,以及支持按照插入顺序来迭代元素的对象。

例如,

  • Map,
  • Set,
  • WeakMap,
  • WeakSet

结构化数据对象

这些对象用来表示和操作结构化的缓冲区数据,或使用JSON(JavaScript Object Notation)编码的数据。例如,ArrayBuffer,SharedArrayBuffer,Atomics,DataView,JSON等。

控制抽象对象

这些对象用来帮助构造代码,尤其是异步代码(例如不使用深度嵌套的回调)。

例如

  • Iterator
  • AsyncIterator
  • Promise
  • GeneratorFunction
  • AsyncGeneratorFunction
  • Generator
  • AsyncGenerator
  • AsyncFunction

反射对象

这些对象用来实现对对象的底层操作,或创建和操作代理对象。例如,Reflect,Proxy等。 Reflect是ES6引入的,Reflect对象提供了一系列的静态方法,它们与对象的基本操作一一对应,例如Reflect.get(),Reflect.set(),Reflect.has()等。这些方法的作用是将对象的操作转为函数调用,从而可以在函数中实现更灵活的逻辑:

const person = {
    name: '1+1=10',
};

console.log(person.name); // 1+1=10
console.log(person['name']); // 1+1=10

console.log(Reflect.get(person, 'name')); // 1+1=10
console.log(Reflect.has(person, 'name')); // true

Reflect.deleteProperty(person, 'name');
console.log(Reflect.has(person, 'name')); // false

Proxy对象用于创建一个对象的代理,它接受两个参数,第一个是目标对象,第二个是一个处理器对象,它包含了一些陷阱(trap)函数,用于拦截目标对象的基本操作。这些陷阱函数的名称和Reflect对象的方法一一对应,例如get,set,has等。

const person = {
    name: '1+1=10',
};

// Create a proxy object
const personProxy = new Proxy(person, {
    get(target, property) {
        console.log(`Getting property: ${property}`);
        return Reflect.get(target, property);
    },
    set(target, property, value) {
        console.log(`Setting property: ${property} to ${value}`);
        return Reflect.set(target, property, value);
    },
    has(target, property) {
        console.log(`Checking if property exists: ${property}`);
        return Reflect.has(target, property);
    },
    deleteProperty(target, property) {
        console.log(`Deleting property: ${property}`);
        return Reflect.deleteProperty(target, property);
    },
});

console.log(personProxy.name); // Getting property: name, 1+1=10
console.log(personProxy['name']); // Getting property: name, 1+1=10

console.log(Reflect.get(personProxy, 'name')); // Getting property: name, 1+1=10
console.log(Reflect.has(personProxy, 'name')); // Checking if property exists: name, true

Reflect.deleteProperty(personProxy, 'name'); // Deleting property: name
console.log(Reflect.has(personProxy, 'name')); // Checking if property exists: name, false

国际化对象

这些对象用来支持多语言处理。例如,Intl,Intl.Collator,Intl.DateTimeFormat,Intl.NumberFormat等。

例子:

// Create a new date object
const date = new Date();

const formattedDate_en_US = new Intl.DateTimeFormat('en-US').format(date);
console.log(formattedDate_en_US);

const formattedDate_zh_CN = new Intl.DateTimeFormat('zh-CN').format(date);
console.log(formattedDate_zh_CN);

结果

1/20/2024
2024/1/20

注,截至目前,QJSEngine不支持Intl。

参考

Comments