本文整理了我对 JavaScript 的一些理解,试将零散的知识归总。此文非语法整理,内容偏中高级,如有纰漏或错误,请予以指正。
开始之前,介绍一款好用的低代码平台——JNPF 快速开发平台。近年在市场表现和产品竞争力方面表现较为突出,采用的是最新主流前后分离框架(SpringBoot+Mybatis-plus+Ant-Design+Vue3)。代码生成器依赖性低,灵活的扩展能力,可灵活实现二次开发。
为了支撑更高技术要求的应用开发,从数据库建模、Web API 构建到页面设计,与传统软件开发几乎没有差异,只是通过低代码可视化模式,减少了构建“增删改查”功能的重复劳动,还没有了解过低代码的伙伴可以尝试了解一下。
有了它,开发人员在开发过程中就可以轻松上手,充分利用传统开发模式下积累的经验。所以低代码平台对于程序员来说,有着很大帮助。
在 JavaScript 的语法层面,除了?undefined
?和?null
?一切皆对象,字面量也是对象,而?null
?的类型也是对象:
'foo'.substring(1); 3.1415926.toFixed(2); typeof null; // 'object'
JavaScript 语言中内置了一些对象用来辅助用户编程,它们均是?函数对象
?,如:
解析引擎中创建了诸多内建类型,它们是实现 JavaScript 各类型的数据结构。
基本类型的字面量创建方式会直接调用解析引擎来创建 JavaScript 对象,它不是内置函数对象的实例:
var foo = 'foo'; console.log(foo instanceof String); // false foo = new String('foo'); console.log(foo instanceof String); // true
对象(这里指语法层面的对象)、正则、数组等的字面量创建方式会调用内置函数对象来创建实例:
var foo = {}; console.log(foo instanceof Object); // true foo = new Object(); console.log(foo instanceof Object); // true
归纳如下:
任何JS对象均需要由函数对象创建。函数对象是在普通对象的基础上增加了内建的属性?[[Call]]
?和?[[Construct]]
?,这一过程由解释器完成,两个属性均指向解释器的内建函数:[[Call]] 用于函数调用,使用操作符?()
?时执行;[[Construct]] 用于构造对象,使用操作符?new
?时执行。
语法层面上,函数对象也是由其它函数对象(或自己)创建的,使用?function
?关键字可以创建用户自定义函数对象。最上游的对象是?Function
?。
当对象被创建后,解释器为对象增加?constructor
?属性指向创建它的函数对象。
原型对象通常由内置函数对象?Object
?创建,它通常是一个普通对象,但也可能是函数对象。
任何对象都有内建属性?[[Prototype]]
?用来指向其原型对象,有些解释器(如V8)会将其开放为?__proto__
?属性供用户代码调用。函数对象有开放属性?prototype
?,用来表示通过函数对象构建的对象的原型。
以下条件总是为 true :
函数对象.prototype === 该函数对象创建的对象.__proto__
示例如下代码的原型关系:
function Foo(){ this.foo = 'foo'; }; Foo.prototype.bar = 'bar'; var f1 = new Foo(); var f2 = new Foo();
对象指向原型对象的层层链条构成原型链,对象查找属性时沿着原型链向上游找。
通常情况下,Function.prototype
?为解析引擎创建的空函数,Object.prototype
?为解析引擎创建的空对象。
示例如下代码:
function Foo(){}; var foo = new Foo();
再加上内置函数对象 String,其关系如下:
有如下规律:
函数生命周期包括:
执行上下文(Execution Context)
?是对可执行代码的抽象,某特定时刻下它们是等价的。发生函数调用的时候,正在执行的上下文被中断并将新的执行上下文压入执行上下文栈中,调用结束后(return 或 throw Error)新的上下文从栈中弹出并继续执行之前的上下文。栈底总是全局执行上下文
:
变量对象(Variable Object)是执行上下文中的一种数据结构,用来存储:
变量对象为抽象概念,其实现分两种情况:
一、全局执行上下文中的变量对象使用全局对象自身实现,因此全局变量可以通过相应的变量对象访问到:
var foo = 'foo' alert(window.foo);
二、函数执行上下文中的变量对象为活动对象(Activation Object),用户代码无法直接访问它。
函数执行前会先为其创建执行环境:
示例以下代码的执行过程:
function foo(foo1, foo2) { var foo3 = 'foo3'; var foo4 = function () {}; this.foo5 = 'foo5'; function foo6() {}; foo6(); } foo('foo1', 'foo2', 'more');
1) 创建执行环境
该过程重点是创建?活动对象
?的命名属性:
2) 依次执行代码
理解了函数执行过程便可以解释局部变量的初始化时机问题:
var foo = 'global'; function bar() { alert(foo); // undefined var foo = 'local'; } bar();
同时也解释了两种函数声明方式的区别:
foo(); // foo bar(); // TypeError: bar is not a function. function foo() { console.log('foo'); } var bar = function () { console.log('bar'); };
根据活动对象的属性填充顺序,也可以解释:
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
示例代码如下:
var x = 1; function foo() { var y = 2; function bar() { var z = 3; alert(x + y + z); } bar(); } foo(); // 6
其作用域相关的属性创建过程如下:
其中函数对象的内部属性?[[Scope]]
?在某些解释器中实现为?__parent__
?并开放给用户代码。执行上下文中的?Scope
?属性构成?作用域链,其实现未必像图中所示使用数组,也可以使用链表等数据结构,ECMAScript 规范对解释器的实现机制未做规定。
变量查找时沿着作用域链向上游查找。例如在函数 bar 中查找 x 时,会依次查找:1)bar的活动对象;2)foo的活动对象;3)全局对象,最终在全局对象中找到。
ECMAScript 使用静态词法作用域:当函数对象创建时,其上层上下文数据(变量对象)保存在内部属性 [[Scope]] 中,即函数在创建的时候就保存了上层上下文的作用域链,不管函数会否被调用。因此所有的函数都是一个闭包(除了 Function 构造器创建的函数)。不过,出于优化目的,当函数不使用自由变量时,引擎实现可能并不保存上层作用域链。
自由变量是在函数内使用的一种变量:它既不是函数的参数,也不是其局部变量。
[[Scope]] 属性是指向变量对象的引用,同一上下文创建的多个闭包共用该变量对象。因此,某个闭包对其变量的修改会影响到其他闭包对其变量的读取:
var fooClosure; var barClosure; function foo() { var x = 1; fooClosure = function () { return ++x; }; barClosure = function () { return --x; }; } foo(); alert(fooClosure()); // 2 alert(barClosure()); // 1
函数执行时,变量对象的属性变化如下:
可以解释此常犯错的情况:
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, 而不是 0 data[1](); // 3, 而不是 1 data[2](); // 3, 而不是 2
通过创建多个变量对象(方式一)或使用函数对象的属性(方式二)可以解决此问题:
// 方式一 var data = []; for (var k = 0; k < 3; k++) { data[k] = (function (x) { return function () { alert(x); }; })(k); } // 方式二 var data = []; for (var k = 0; k < 3; k++) { (data[k] = function () { alert(arguments.callee.x); }).x = k; }
从理论角度讲,ECMAScript 中所有的函数都是闭包。然而实践中,以下函数才算是闭包:
不使用 var 关键字创建的只是全局对象的属性(全局执行上下文中的变量对象使用全局对象自身实现),它并不是一个变量。可以用如下代码检测区别:
alert(a); // undefined alert(b); // Can't find variable: b b = 10; var a = 20;
1)?函数声明在程序级别或另一函数的函数体:
function foo() { // ... } function globalFD() { function innerFD() {} }
2)?函数表达式在表达式的位置:
var foo = function () { // ... }; (function foo() {}); [function foo() {}]; 1, function foo() {}; var bar = (foo % 2 == 0 ? function () { alert(0); } : function () { alert(1); } ); // bar 为函数表达式: foo(function bar() { alert('foo.bar'); });
函数表达式的作用是避免对变量对象造成污染。
3)Function构造器的 [[Scope]] 属性中只包含全局对象:
var x = 10; function foo() { var x = 20; var y = 30; var bar = new Function('alert(x); alert(y);'); bar(); // 10, "y" is not defined }