Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

函数 #6

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

函数 #6

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

函数定义

函数有函数声明和函数表达式两种,其中函数表达式又可分为具名和匿名形式

函数声明

函数声明:

一个函数定义(也称为函数声明,或函数语句)由一系列的function关键字组成,依次为:

  • 函数的名称。
  • 函数参数列表,包围在括号中并由逗号分隔。
  • 定义函数的 JavaScript 语句,用大括号{}括起来。

ECMAScript 语法表示:

FunctionDeclaration :
function Identifier ( FormalParameterListopt ) { FunctionBody }

比如说:

function foo(val) {
	return val;
}

函数表达式

函数声明在语法上是一个语句,但函数也可以由函数表达式创建(函数名可以省略)。简单一点说就是当由 function 关键字组成的语句处在表达式时,这样的形式就是函数表达式。令 Fn 等于 function Identifieropt ( FormalParameterListopt ) { FunctionBody } 当 Fn 处在表达式时,Fn 就是 函数表达式。

ECMAScript 语法表示:

FunctionDeclaration :
function Identifieropt ( FormalParameterListopt ) { FunctionBody }

常见的函数表达式:

let func = function _func(val) {
	return val;
} 
// 或者

let func = function (val) {
	return val;
}

只要 Fn 处在表达式中,Fn 就是函数表达式,所以下面这些也是函数表达式:

(function foo() {})

[function bar() {}]

1, function baz() {}

创建函数对象

在执行函数之前,会创建函数对象。

下面是 ECMAScript 对创建函数对象规定:

指定 FormalParameterList 为可选的 参数列表,指定 FunctionBody 为 函数体,指定 Scope 为词法环境,Strict 为布尔标记,按照如下步骤构建函数对象:

  1. 创建一个新的 ECMAScript 原生对象,令 F 为此对象。
  2. 依照 8.12 描述设定 F 的除 [[Get]] 以外的所有内部方法。
  3. 设定 F 的 [[Class]] 内部属性为 "Function"
  4. 设定 F 的 [[Prototype]] 内部属性为 15.3.3.1 指定的标准内置 Function 对象的 prototype 属性。
  5. 依照 15.3.5.4 描述,设定 F 的 [[Get]] 内部属性。
  6. 依照 13.2.1 描述,设定 F 的 [[Call]] 内部属性。
  7. 依照 13.2.2 描述,设定 F 的 [[Construct]] 内部属性。
  8. 依照 15.3.5.3 描述,设定 F 的 [[HasInstance]] 内部属性。
  9. 设定 F 的 [[Scope]] 内部属性为 Scope 的值。
  10. 令 names 为一个列表容器,其中元素是以从左到右的文本顺序对应 FormalParameterList 的标识符的字符串。
  11. 设定 F 的 [[FormalParameters]] 内部属性为 names。
  12. 设定 F 的 [[Code]] 内部属性为 FunctionBody
  13. 设定 F 的 [[Extensible]] 内部属性为 true
  14. 令 len 为 FormalParameterList 指定的形式参数的个数。如果没有指定参数,则令 len 为 0
  15. 以参数 "length"、属性描述符 {[[Value]]: len, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false}、false 调用 F 的 [[DefineOwnProperty]] 内部方法。
  16. 令 proto 为仿佛使用 new Object() 表达式创建新对象的结果,其中 Object 是标准内置构造器名。
  17. 以参数 "constructor"、属性描述符 {[[Value]]: F, { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true}、false 调用 proto 的 [[DefineOwnProperty]] 内部方法。
  18. 以参数 "prototype"、属性描述符 {[[Value]]: proto, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false}、false 调用 F 的 [[DefineOwnProperty]] 内部方法。
  19. 如果 Strict 是 true,则
    1. 令 thrower 为 [[ThrowTypeError]] 函数对象(13.2.3)。
    2. 以参数 "caller"、属性描述符 {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}、false 调用 F 的 [[DefineOwnProperty]] 内部方法。
    3. 以参数 "arguments"、属性描述符 {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}、false 调用 F 的 [[DefineOwnProperty]] 内部方法。
  20. 返回 F。

注:每个函数都会自动创建一个 prototype 属性,以满足函数会被当作构造器的可能性。

总的来说就是先创建一个 F 对象,然后不断添加不同的属性(内部属性),返回 F。

当调用函数时,会调用 [[Call]] 内置方法,使用 new 运算符时,会调用 [[Construct]] 内置方法。具体操作会在后面阐述。其他的属性以后遇见了在说,这里重点关注 [[Scope]] 属性。

设定 F 的 [[Scope]] 内部属性为 Scope 的值。

[[Scope]] 内部属性为 Scope 值。这个属性和作用域链相关。这里简单说一下

var a = 1;

function foo() {
	console.log(a); // 1
}

foo();

在 foo 函数的作用域中是没有 a 这个变量的,那么就会按照作用域链往上查找,具体在哪里查找呢?就是在函数的 [[Scope]] 中查找。

Scope 值在函数定义有涉及

函数声明的 Scope 值:

运行中的执行环境的 VariableEnvironment 传递为 Scope

匿名函数表达式的 Scope 值:

运行中的执行环境的 LexicalEnvironment 传递为 Scope

函数声明和匿名函数表达式的 [[Scope ]]

执行环境后面会做专门介绍。函数声明和匿名函数表达式的 Scope 可以理解为函数定义时所在的词法环境(在上面的例子中,词法环境包括了 a 变量,所以才能在 foo 函数中查找到 a)。

具名函数表达式的 [[Scope ]]

具名函数表达式的 Scope 值:

具名函数表达式的 Scope 值和其它两种不太一样。

产生式 FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody } 的解释执行如下:

  1. 令 funcEnv 为以运行中执行环境的 LexicalEnvironment 为参数调用 NewDeclarativeEnvironment 的结果。
  2. 令 envRec 为 funcEnv 的环境记录项。
  3. Identifier 的字符串值为参数调用 envRec 的具体方法 CreateImmutableBinding(N)。
  4. 令 closure 为依照 13.2,指定 FormalParameterListopt 为参数,指定 FunctionBody 为 函数体,创建一个新函数对象的结果。传递 funcEnv 为 Scope
  5. Identifier 的字符串值和 closure 为参数调用 envRec 的具体方法 InitializeImmutableBinding(N,V)。
  6. 返回 closure。

第一点表明 funcEnv 是 NewDeclarativeEnvironment 返回的词法环境(词法环境由一个环境记录项和可能为空的外部词法环境引用构成)。NewDeclarativeEnvironment 规则:

当调用 NewDeclarativeEnvironment 抽象运算时,需指定一个词法环境 E,其值可以为 null,此时按以下步骤进行:

  1. 令 env 为一个新建的词法环境
  2. 令 envRec 为一个新建的声明式环境数据,该环境数据不包含任何绑定。
  3. 令 env 的环境数据为 envRec。
  4. 令 env 的外部词法环境引用至 E。
  5. 返回 env。

根据规则,funcEnv 是环境数据(声明式环境数据)为空,外部词法环境是 LexicalEnvironment(也就是函数定义所在的词法环境)的词法环境(词法环境会在执行环境中介绍)。

第二点让 envRec 绑定到 funcEnv 的环境记录项

第三点就是处理函数表达式的名称。调用 envRec 的 CreateImmutableBinding 方法。CreateImmutableBinding 方法会在 envRec中创建一个以 Identifier 名称的不可变绑定并初始化为 undefined。在第五点的时候会将 Identifier 绑定的值修改为 closure (函数对象)。

identifier 是不可变绑定,所以在具名函数表达式中是不能修改 identifier 值:

let foo = function _foo() {
	_foo = 12; // 在严格模式下会报错
	console.log(_foo); // [Function: _foo]
}

foo();

第四点会将第一点创建的 funcEnv 传为 Scope。

那么具名函数表达式的 [[Scope]] 属性的值和其他两种有什么区别?在我们实际编程中有哪些体现?

其他两种函数的 [[Scope]] 属性的值 Scope 都是函数定义所在的词法环境,但是具名函数表达式 [[Scope]] 的值 Scope 是新创建的一个词法环境,这个新创建的词法环境的环境记录项就只有 Identifier(函数对象) 绑定,外部词法环境才是函数定义所在的词法环境。假设 globalScope 是全局词法环境,newScope 是具名函数表达式新建 Scope,FnScope 是具名函数表达式 Scope,那么具名函数表达式查找变量的顺序是 FnScope ---> newScope ---> globalScope。具名函数表达式名称变量是在 newScope 这里绑定的。

在实际中的体现:

var count = 0;

var foo = function _foo() {
	if (++count > 3) {
		return;
	}
	
	console.log(count);
	_foo();
};

// _foo(); // ReferenceError: _foo is not defined
foo(); // 1 2 3

具名函数表达式 _foo 可以调用自身,但是不能在全局调用,也就是在全局的词法环境中找不到 _foo 变量。因为 _foo 是存在 newScope 中的。

上面这个是 ECMAScript 规定的,但是在实际 JS 引擎实现中可能不一样。

在 Rhino 中,具名函数表达式名字是直接保存在函数自己的 Scope 中,在 JScript 中,直接保存在上层作用域中,所以在函数外面也可以访问(在 IE8 环境下运行)

var count = 0;

var foo = function _foo() {
	if (++count > 3) {
		return;
	}
	
	console.log(count);
	_foo();
};

_foo(); // 1 2 3

函数调用

函数调用方式如 Foo()

产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :

  1. 令 ref 为解释执行 MemberExpression 的结果。
  2. 令 func 为 GetValue(ref)。
  3. 令 argList 为解释执行 Arguments 的结果,产生参数值们的内部列表(参见 11.2.4)。
  4. 如果 Type(func) 不是 Object,抛出一个 TypeError 异常。
  5. 如果 IsCallable(func) 为 false,抛出一个 TypeError 异常。
  6. 如果 Type(ref) 为 Reference,那么
    1. 如果 IsPropertyReference(ref) 为 true,那么
      1. 令 thisValue 为 GetBase(ref)。
    2. 否则,ref 的基值是一个环境记录项。
      1. 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果。
  7. 否则,Type(ref) 不是 Reference。
    1. 令 thisValue 为 undefined
  8. 返回调用 func 的 [[Call]] 内置方法的结果,传入 thisValue 作为 this 值和列表 argList 作为参数列表。

第一二点,令 func 为 GetValue(ref) 结果,这里 func 就是函数对象。

第三点,令 argList 为实参列表

第四五点,对 func 判断

var foo = 0;
foo(); // TypeError: foo is not a function(Type(foo) 不是 Object)
var foo = {};
foo(); // TypeError: foo is not a function (Type(foo) 没有 [[call]] 内部方法)

第六七点,和 this 相关,在 this 章节说明。

第八点,以 thisValue 作为 this 值,argList 列表作为参数列表调用 [[Call]] 内置方法。

在创建函数对象的时候,会内置很多属性方法,其中就有 [[Call]] 内置方法。

[[Call]]

[[Call]] 内部方法

当用一个 this 值、一个参数列表调用函数对象 F 的 [[Call]] 内部方法,采用以下步骤:

  1. 用 F 的 [[FormalParameters]] 内部属性值、参数列表 args、10.4.3 描述的 this 值来建立函数代码的一个新执行环境,令 funcCtx 为其结果。
  2. 令 result 为 FunctionBody(也就是 F 的 [[Code]] 内部属性)解释执行的结果。如果 F 没有 [[Code]] 内部属性或其值是空的 FunctionBody,则 result 是 (normal, undefined, empty)。
  3. 退出 funcCtx 运行环境,恢复到之前的执行运行环境。
  4. 如果 result**.type** 是 throw 则抛出 result**.value**。
  5. 如果 result**.type** 是 return 则返回 result**.value**。
  6. 否则 result**.type** 必定是 normal。返回 undefined

第一点,以 [[FormalParameters]] 内部属性值,参数列表 args,this 值建立新执行环境 funcCtx。其中 [[FormalParameters]] 内部属性值是形参,args 是实参。

第二点,解释执行 FunctionBody ,结果为 result。如果没有 FunctionBody,result 是(normal,undefined,empty)

第三点,执行完函数之后,恢复到上一个执行环境状态。

最后三点,关于执行结果 result 的规定。通过 result.type 值决定返回什么,但是 result.type 是一个内部值,我们不能访问。

在解释执行 FunctionBody 时,还会有一个步骤:进入函数代码。

进入函数代码

进入函数代码

当控制流根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入函数代码的执行环境时,执行以下步骤:

  1. 如果函数代码严格模式下的代码,设 this 绑定 为 thisArg。
  2. 否则如果 thisArg 是 nullundefined,则设 this 绑定全局对象
  3. 否则如果 Type(thisArg) 的结果不为 Object,则设 this 绑定ToObject(thisArg)。
  4. 否则设 this 绑定 为 thisArg。
  5. 以 F 的 [[Scope]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。
  6. 词法环境组件 为 localEnv。
  7. 变量环境组件 为 localEnv。
  8. 令 code 为 F 的 [[Code]] 内部属性的值。
  9. 10.5 描述的方案,使用函数代码 code 和 argumentList 执行声明式绑定初始化化步骤。

前四点和 this 值相关,在 this 章节说明。

第五点,以 F 的 [[Scope]] 内部属性新建一个词法环境 localEnv,这个词法环境的环境数据还没有任何绑定。

第六七点,词法环境组件和变量环境组件都为 localEnv。(词法环境组件和变量环境组件在执行环境章节中说明)。

第八点,code 设为 [[Code]] 内部属性值,也就是 FunctionBody。

第九点,进行声明式绑定初始化。

声明式绑定初始化

声明式绑定初始化,在以下三个地方需要执行:

  • 进入全局代码
  • 进入 eval 代码
  • 进入函数代码

这里重点关注进入函数代码相关部分

声明式绑定初始化

每个执行环境都有一个关联的 变量环境组件。当在一个执行环境下评估一段 ECMA 脚本时,变量和函数定义会以绑定的形式添加到这个 变量环境组件环境记录中。对于函数代码,参数也同样会以绑定的形式添加到这个 变量环境组件环境记录中。

选择使用哪一个、哪一类型的环境记录来绑定定义,是由执行环境下执行的 ECMA 脚本的类型决定的,而其它部分的逻辑是相同的。当进入一个执行环境时,会按以下步骤在 变量环境组件 上创建绑定,其中使用到调用者提供的代码设为 code,如果执行的是函数代码,则设参数列表为 args:

  1. 令 env 为当前运行的执行环境的变量环境组件环境记录项
  2. 如果 code 是 eval 代码,则令 configurableBindings 为 true,否则令 configurableBindings 为 false
  3. 如果代码是严格模式下的代码,则令 strict 为 true,否则令 strict 为 false
  4. 如果代码为函数代码,则:
    1. 令 func 为通过 [[Call]] 内部属性初始化 code 的执行的函数对象。令 names 为 func 的 [[FormalParameters]] 内部属性的值。
    2. 令 argCount 为 args 中元素的数量。
    3. 令 n 为数值 0
    4. 按列表顺序遍历 names,对于每一个字符串 argName:
      1. 令 n 的值为 n 当前值加 1
      2. 如果 n 大于 argCount,则令 v 为 undefined,否则令 v 为 args 中的第 n 个元素。
      3. 以 argName 为参数,调用 env 的 HasBinding 具体方法,并令 argAlreadyDeclared 为调用的结果。
      4. 如果 argAlreadyDeclared 的值为 false,以 argName 为参数调用 env 的 CreateMutableBinding 具体方法。
      5. 以 argName、v 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法。
  5. 按源码顺序遍历 code,对于每一个 FunctionDeclaration f:
    1. 令 fn 为 FunctionDeclaration f 中的 Identifier
    2. 第 13 章中所述的步骤初始化 FunctionDeclaration f ,并令 fo 为初始化的结果。
    3. 以 fn 为参数,调用 env 的 HasBinding 具体方法,并令 argAlreadyDeclared 为调用的结果。
    4. 如果 argAlreadyDeclared 的值为 false,以 fn 和 configurableBindings 为参数调用 env 的 CreateMutableBinding 具体方法。
    5. 否则如果 env 是全局环境的环境记录对象,则:
      1. 令 go 为全局对象。
      2. 以 fn 为参数,调用 go 和 [[GetProperty]] 内部方法,并令 existingProp 为调用的结果。
      3. 如果 existingProp.[[Configurable]] 的值为 true,则:
        1. 以 fn、由 {[[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: configurableBindings } 组成的属性描述符true 为参数,调用 go 的 [[DefineOwnProperty]] 内部方法。
      4. 否则如果 IsAccessorDescriptor(existingProp) 的结果为真,或 existingProp 的特性中没有 {[[Writable]]: true, [[Enumerable]]: true},则:
        1. 抛出一个 TypeError 异常。
    6. 以 fn、fo 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法。
  6. "arguments" 为参数,调用 env 的 HasBinding 具体方法,并令 argumentsAlreadyDeclared 为调用的结果。
  7. 如果 code 是函数代码,并且 argumentsAlreadyDeclared 为 false,则:
    1. 以 fn、names、args、env 和 strict 为参数,调用 CreateArgumentsObject 抽象运算函数,并令 argsObj 为调用的结果。
    2. 如果 strict 为 true,则:
      1. 以字符串**"arguments"**为参数,调用 env 的 CreateImmutableBinding 具体方法。
      2. 以字符串 "arguments" 和 argsObj 为参数,调用 env 的 InitializeImmutableBinding 具体函数。
    3. 否则:
      1. 以字符串 **"arguments"**为参数,调用 env 的 CreateMutableBinding 具体方法。
      2. 以字符串**"arguments"**、argsObj 和 false 为参数,调用 env 的 SetMutableBinding 具体函数。
  8. 按源码顺序遍历 code,对于每一个 VariableDeclarationVariableDeclarationNoIn 表达式作为 d 执行:
    1. 令 dn 为 d 中的标识符
    2. 以 dn 为参数,调用 env 的 HasBinding 具体方法,并令 varAlreadyDeclared 为调用的结果。
    3. 如果 varAlreadyDeclared 为 false,则:
      1. 以 dn 和 configurableBindings 为参数,调用 env 的 CreateMutableBinding 具体方法。
      2. 以 dn、undefined 和 strict 为参数,调用 env 的 SetMutableBinding 具体方法。

声明式绑定初始化和我们长说的变量提升有关。

第一点,令 env 为当前运行的执行环境的变量环境组件的环境记录项。

第二点,和 eval 相关,忽略。

第三点,判断是否为严格模式。

处理函数参数

第四点,处理实参赋值给形参。总的来说是以 argName,v 和 strict 为参数调用 SetMutableBinding 进行绑定。argName 是形参标识符,v 是实参值,strict 表示是否为严格模式。不过其中处理了一些其他情况。

  1. 令 names 为形参列表
  2. 令 argCount 为实参数量
  3. 设 n = 0
  4. 按列表顺序遍历 names,对于每一个 names 字符串 argName:
    1. 令 n 值在 n 当前值加 1。
    2. 当 n(形参第几个元素) 大于 argCount(实参数量),令 v 为 undefined,否则令 v 为 args(实参) 中的第 n 个元素。(如果实参数量大于形参,多出的实参不处理)
    3. 判断 argName 在 env 是否有绑定。
    4. 如果没有绑定,以 argName 为参数调用 env 的 CreateMutableBinding 方法。(没有绑定,重新创建一个可变绑定)。
    5. 通过 env 的 SetMutableBinding 方法给 argName 赋值。

在真正解释执行前函数参数是已经有值了。

function foo(a) {
	console.log(a); // 10
	console.log(b); // undefined
	
	var b = 1;
}

foo(10);

另外一个例子:

function foo(a) {
	console.log(a); // 10
	var a = 12;
}

foo(10);

因为函数中的变量绑定初始化的时间(在最后一步)晚于实参的绑定初始化。当绑定变量时,因为执行环境中已经有 a 的绑定了,所以不对变量 a 做处理,最后执行环境中剩下的是实参 a 的绑定。

处理函数声明

第五点,处理函数声明。如果在遍历 code 中,遇见函数声明 f:

  1. 令 fn 为函数声明 f 的标识符。

  2. 根据函数声明创建一个函数对象 fo。

  3. 通过 env 的 HasBinding 方法查看 env 是否绑定 fn。

  4. 如果没有绑定,以 fn 和 configurableBindings 为参数调用 env 的 CreateMutableBinding 方法创建一个可变绑定。其中 configurableBindings 参数表示是否可以删除。

    function foo() {
    	function func() {}
    	
    	var res = delete func;
    	console.log(res) // false
    }
    
    foo();
    

    不能删除 func 函数的,所以 configurableBindings 为 false。

  5. 当函数声明在全局下的处理

  6. 通过 env 的 SetMutableBinding 方法绑定 fn 值为 fo。

所以在函数执行前,函数声明的标识符已经存在函数对象。

function foo() {
	console.log(func); // ƒ func() {}
	
	function func() {}
}

foo();

声明式绑定初始化时,是先处理函数声明的,最后才处理其他表示符。

var foo = 1;
console.log(foo) // 1
function foo() {}

foo() // Uncaught TypeError: foo is not a function

原因:首先处理函数声明 foo。然后处理变量 foo 时,发现已经绑定了 foo 标识符,就不做处理。

当执行到 foo = 1 时,实际上是将标识符 foo 重新赋值为 1,此时 foo 不在代表函数,所以执行 foo() 时会报错。

arguments 对象

第六七点,关于 arguments 对象。

首先检查 env 中存在 arguments 绑定没有,如果没有,就通过 CreateArgumentsObject 抽象运算函数创建一个对象 argsObj。

在严格模式下,以 “arguments” 为参数调用 env 的 CreateImmutableBinding 方法创建一个不可变绑定。以 “arguments” 和 argsObj为参数调用 env 的 InitializeImmutableBinding 函数初始化。

在非严格模式下,以 “arguments” 为参数调用 env 的 CreateMutableBinding 方法创建一个可变绑定。以 “arguments” 和 argsObj为参数调用 env 的 SetMutableBinding 函数初始化。

function foo(arguments) {
	console.log(arguments); // 12
}

foo(12);

因为形参中存在 arguments 字符串,而且处理函数参数在处理 arguments 对象之前,此时 env 中已经存在 arguments 绑定了,所以不会创建 arguments 对象。

'use strict';

function foo(a) {
	arguments = 12;
}

foo(1); // SyntaxError: Unexpected eval or arguments in strict mode

在严格模式下,arguments 是不可变的

####创建 Arguments 对象

arguments 对象是由 CreateArgumentsObject 抽象运算函数创建的

CreateArgumentsObject 抽象运算函数

当控制器进入到函数代码的执行环境时,将创建一个 arguments 对象(透过 10.5 指定的方式),除非它作为标识符 arguments 出现在该函数的形参列表中,或者是该函数代码内部的变量声明标识符或函数声明标识符

arguments 对象通过调用抽象方法 CreateArgumentsObject 创建,调用时将以下参数传入:func、names、args、env、strict。将要执行的函数对象作为 func 参数,将该函数的所有形参名加入一个列表,称为 names 参数,将所有传给内部方法 [[Call]] 的实际参数,称为 args 参数,将该函数代码的变量环境称为 env 参数,将该函数代码是否为严格代码作为 strict 参数。当 CreateArgumentsObject 调用时,按照以下步骤执行:

  1. 令 len 为参数 args 的元素个数。
  2. 令 obj 为一个新建的 ECMAScript 对象。
  3. 按照 8.12 章节中的规范去设定 obj 对象的所有内部方法。
  4. 将 obj 对象的内部属性 [[Class]] 设置为 "Arguments"
  5. 令 Object 为标准的内置对象的构造函数(15.2.2)。
  6. 将 obj 对象的内部属性 [[Prototype]] 设置为标准的内置对象的原型对象。
  7. 调用 obj 的内部方法 [[DefineOwnProperty]],将 "length" 传递进去,属性描述符为:{[[Value]]: len, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true},参数为 false
  8. 令 map 为表达式 new Object() 创建的对象,就是名为 Object标准的内置构造函数
  9. 令 mappedNames 为一个空的列表
  10. 令 indx = len - 1
  11. 当 indx>=0 的时候,重复此过程:
    1. 令 val 为 args(维度从 0 开始的列表)的第 indx 维度所在的元素。
    2. 调用 obj 的内部方法 [[DefineOwnProperty]],将 ToString(indx) 传递进去,属性描述符为:{[[Value]]: val, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true},参数为 false
    3. 如果 indx 小于 names 的元素个数,则
      1. 令 name 为 names(维度从 0 开始的列表)的第 indx 维度所在的元素。
      2. 如果 strict 值为 false,且 name 不是一个 mappedNames 元素,则
        1. 将 name 添加到 mappedNames 列表中,作为它的一个元素
        2. 令 g 为调用抽象操作 MakeArgGetter 的结果,其参数为 name 和 env。
        3. 令 p 为调用抽象操作 MakeArgSetter 的结果,其参数为 name 和 env。
        4. 调用 map 对象的内部方法 [[DefineOwnProperty]],将 ToString(indx) 传递进去,属性描述符为:{[[Set]]: p, [[Get]]: g, [[Configurable]]: true},参数为 false
    4. 令 indx = indx - 1
  12. 如果 mappedNames 不为空,则
    1. 将 obj 对象的内部属性 [[ParameterMap]] 设置为 map。
    2. 将 obj 对象的内部方法 [[Get]]、[[dGetOwnProperty]]、[[DefineOwnProperty]]、[[Delete]] 按下面给出的定义进行设置。
  13. 如果 strict 值为 false,则
    1. 调用 obj 对象的内部方法 [[DefineOwnProperty]],将 "callee" 传递进去,属性描述符为:{[[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true},参数为 false
  14. 否则,strict 值为 true,那么
    1. 令 thrower 为 [[ThrowTypeError]] 函数对象(13.2.3)。
    2. 调用 obj 对象的内部方法 [[DefineOwnProperty]],将 "caller" 传递进去,属性描述符为:{[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},参数为 false
    3. 调用 obj 对象的内部方法 [[DefineOwnProperty]],将 "callee" 传递进去,属性描述符为:{[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},参数为 false
  15. 返回 obj。

抽象操作 MakeArgGetter 以字符串 name 和环境记录 env 作为参数被调用时,会创建一个函数对象,当执行完后,会返回在 env 中绑定的 name 的值。执行步骤如下:

  1. 令 body 为字符 "return "、name、";" 的连接字符串。
  2. 返回一个按照 13.2 章节中描述的方式创建的函数对象,它不需要形参列表,以 body 作为它的 FunctionBody,以 env 作为它的 Scope,并且 strict 值为 true

抽象操作 MakeArgSetter 以字符串 name 和环境记录 env 作为参数被调用时,会创建一个函数对象,当执行完后,会给在 env 中绑定的 name 设置一个值。执行步骤如下:

  1. 令 param 为 name 和字符串 "_arg" 的连接字符串。
  2. 令 body 为字符串 "=;";将 替换为 name 的值,将 替换为 param 的值。
  3. 返回一个按照 13.2 章节中描述的方式创建的函数对象,以一个只包含字符串 param 的列表作为它的形参,以 body 作为它的函数体(FunctionBody),以 env 作为它的 Scope,并且 strict 值为 true

arguments 对象的内部方法 [[Get]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P 的条件下被调用时,其执行步骤如下:

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]]
  2. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  3. 如果 isMapped 值为 undefined,则
    1. 令 v 为 arguments 对象的内部默认的 [[Get]] 方法(8.12.3),传入参数 P 的执行结果。
    2. 如果 P 为 "caller",且 v 为严格模式下的 Function 对象,则抛出一个 TypeError 的异常。
    3. 返回 v。
  4. 否则,map 包含一个 P 的形参映射表。
    1. 返回 map 对象的内部方法 [[Get]] 传入参数 P 的执行结果。

arguments 对象的内部方法 [[GetOwnProperty]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P 的条件下被调用时,其执行步骤如下:

  1. 令 desc 为 arguments 对象的内部方法 [[GetOwnProperty]]8.12.1)传入参数 P 的执行结果。
  2. 如果 desc 为 undefined,返回 desc。
  3. 令 map 为 arguments 对象内部属性 [[ParameterMap]] 的值。
  4. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  5. 如果 isMapped 的值不是 undefined,则
    1. 将 desc.[[Value]] 的值设置为 map 对象的内部方法 [[Get]] 传入参数 P 的执行结果。
  6. 返回 desc。

arguments 对象的内部方法 [[DefineOwnProperty]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P,属性描述符为 Desc,布尔标志为 Throw 的条件下被调用时,其执行步骤如下:

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]] 的值。
  2. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  3. 令 allowed 为 arguments 对象的内部方法 [[DefineOwnPropery]]8.12.9)传入参数 P、Desc、false 的执行结果。
  4. 如果 allowed 为 false,则
    1. 如果 Throw 为 true,则抛出一个 TypeError 的异常,否则,返回 false
  5. 如果 isMapped 的值不为 undefined,则
    1. 如果 IsIsAccessorDescriptor(Desc) 为 true,则
      1. 调用 map 对象的内部方法 [[Delete]],传入 P 和 false 作为参数。
    2. 否则
      1. 如果 Desc.[[Value]] 存在,则
        1. 调用 map 对象的内部方法 [[Put]],传入 P、Desc.[[Value]] 和 Throw 作为参数。
      2. 如果 Desc.[[Writable]] 存在,且其值为 false,则
        1. 调用 map 对象的内部方法 [[Delete]],传入 P 和 false 作为参数。
  6. 返回 true

arguments 对象的内部方法 [[Delete]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P,布尔标志为 Throw 的条件下被调用时,其执行步骤如下:

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]] 的值。
  2. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  3. 令 result 为 arguments 对象的内部方法 [[Delete]]8.12.7)传入参数 P 和 Throw 的执行结果。
  4. 如果 result 为 true,且 isMapped 不为 undefined,则
    1. 调用 map 对象的内部方法 [[Delete]],传入 P 和 false 作为参数。
  5. 返回 result。

注: 非严格模式下的函数,arguments 对象以数组索引(参见 15.4 的定义)作为数据属性的命名,其数字名字的值少于对应的函数对象初始时的形参数量,它们与绑定在该函数执行环境中对应的参数共享值。这意味着,改变该属性将改变这些对应的、绑定的参数的值,反之亦然。如果其中一个属性被删除然后再对其重定义,或者其中一个属性在某个访问器属性内部被更改,则这种对应关系将被打破。严格模式下的函数,arguments 对象的属性值就是传入该函数的实际参数的简单拷贝,它们与形参之间的值不存在动态的联动关系。

注: ParameterMap 对象和它的属性值被作为说明 arguments 对象对应绑定参数的装置。ParameterMap 对象和它的属性值对象不能直接被 ECMAScript 代码访问。作为 ECMAScript 的实现,不需要实际创建或使用这些对象去实现指定的语义。

注: 严格模式下函数的 Arguments 对象定义的非可配置的访问器属性,"caller""callee",在它们被访问时,将抛出一个 TypeError 的异常。在非严格模式下,"callee" 属性具有非常明确的意义,"caller" 属性有一个历史问题,它是否被提供,视为一个由实作环境决定的,在具体的 ECMAScript 实作进行扩展。在严格模式下对这些属性的定义的出现是为了确保它们俩谁也不能在规范的 ECMAScript 实作中以任何方式被定义。

arguments 对象会简单将实参赋值到对象的属性上,arguments 对象的内部属性 [[ParameterMap]] 的值 map 和形参相关,在非严格模式下,一般情况,arguments 对象和形参共享值,在严格模式下,arguments 对象和形参没有任何关系。

第一点,令 len 等于 args 元素个数。

第二到七点,新建的 ECMAScript 对象 obj 并添加相关属性。

第八点,令 map 为 new Object() 创建的对象。

第九点,令 mappedNames 为空列表。

第十点,令 indx = len - 1。

第十一点,当 indx >= 0 时,这一步是循环过程,处理 arguments 对象和实参,形参的关系。

  1. 令 val 为 args(维度从 0 开始的列表,就是实参列表)的第 indx 维度所在的元素。
  2. 将 val 添加到 obj[ToString(indx)]。通过 obj 的内部方法 [[DefineOwnProperty]] 添加。
  3. 在非严格模式下,如果 indx 小于 names(形参列表)的元素个数,则
    1. 令 name 为 names(维度从 0 开始的列表)的第 indx 维度元素。
    2. 在非严格模式下,且 name 不是 mappedNames 元素,则
      1. 将 name 添加到 mappedNames 列表中。
      2. 将 name 的取值变为一个函数对象(MakeArgGetter(name, env) 结果)。
      3. 将 name 的赋值变为一个函数对象(MakeArgSetter(name, env) 结果)。
      4. 将 name 添加到 map[[ToString(indx)]]。通过 map 内部方法 [[DefineOwnProperty]]。属性描述符 {[[Set]]: p, [[Get]]: g, [[Configurable]]: true}。
  4. 令 indx = indx - 1

在 11.2 步骤,将实参赋值到 obj[ToString(indx)],在 11.3 步骤,将形参赋值到 map[[ToString(indx)]],在 12.1 步骤,会将 map 添加到 obj[[ParameterMap]]。

第十二点,在非严格模式下,如果 mappedNames 不为空,则

  1. 将 map 添加到 obj[[ParameterMap]].
  2. 设置 obj 对象内部方法 [[Get]]、[[GetOwnProperty]]、[[DefineOwnProperty]]、[[Delete]]。具体见后面

第十三、十四点,在非严格模式下,规定 arguments 对象的 calleecaller 属性(caller 已经完全删除,在严格模式下,callee 是不能被访问的)。

[[Get]]

arguments 对象的内部方法 [[Get]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P 的条件下被调用时,其执行步骤如下:

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]]
  2. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  3. 如果 isMapped 值为 undefined,则
    1. 令 v 为 arguments 对象的内部默认的 [[Get]] 方法(8.12.3),传入参数 P 的执行结果。
    2. 如果 P 为 "caller",且 v 为严格模式下的 Function 对象,则抛出一个 TypeError 的异常。
    3. 返回 v。
  4. 否则,map 包含一个 P 的形参映射表。
    1. 返回 map 对象的内部方法 [[Get]] 传入参数 P 的执行结果。

当 arguments[P] 时,首先判断 map 是否有 P 属性,如果没有,调用 arguments 对象的内部默认的 [[Get]] 方法,返回结果(取实参的值)。如果 map 有 P 属性,调用 map 对象内部方法 [[Get]],返回结果(取形参的结果)。

function foo(a) {
	console.log(arguments[0]);
}

foo(1); // 1

因为在 map 对象上存在 “0” 属性,所以 arguments[0] 取得是 map[0] 值,也就是 a 的值。

function foo(a) {
	console.log(arguments[1]);
}

foo(1, 2); // 2

因为在 map 对象上不存在 “1” 属性,所以 arguments[1] 取得是自己的 “1“ 属性的值,也就是形参列表中第二个元素的值。

[[GetOwnProperty]]

arguments 对象的内部方法 [[GetOwnProperty]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P 的条件下被调用时,其执行步骤如下:

  1. 令 desc 为 arguments 对象的内部方法 [[GetOwnProperty]]8.12.1)传入参数 P 的执行结果。
  2. 如果 desc 为 undefined,返回 desc。
  3. 令 map 为 arguments 对象内部属性 [[ParameterMap]] 的值。
  4. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  5. 如果 isMapped 的值不是 undefined,则
    1. 将 desc.[[Value]] 的值设置为 map 对象的内部方法 [[Get]] 传入参数 P 的执行结果。
  6. 返回 desc。

desc 为 arguments.[[GetOwnProperty]](P) 结果。

如果 desc 为 undefined,直接返回 desc。

令 isMapped 为 map.[[GetOwnProperty]](P) 结果。

如果 isMapped 为 undefined,直接返回 desc。如果 isMapped 不为 undefined,将 desc.[[Value]] 修改为 map.[[Get]](P) 的结果。

返回 desc。

function foo(a) {
	console.log(Object.getOwnPropertyDescriptor(arguments, '0'));
  a = 10;
  console.log(Object.getOwnPropertyDescriptor(arguments, '0'));
}

foo(1); 
// {value: 1, writable: true, enumerable: true, configurable: true}
// {value: 10, writable: true, enumerable: true, configurable: true}

[[DefineOwnProperty]]

arguments 对象的内部方法 [[DefineOwnProperty]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P,属性描述符为 Desc,布尔标志为 Throw 的条件下被调用时,其执行步骤如下:

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]] 的值。
  2. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  3. 令 allowed 为 arguments 对象的内部方法 [[DefineOwnPropery]]8.12.9)传入参数 P、Desc、false 的执行结果。
  4. 如果 allowed 为 false,则
    1. 如果 Throw 为 true,则抛出一个 TypeError 的异常,否则,返回 false
  5. 如果 isMapped 的值不为 undefined,则
    1. 如果 IsIsAccessorDescriptor(Desc) 为 true,则
      1. 调用 map 对象的内部方法 [[Delete]],传入 P 和 false 作为参数。
    2. 否则
      1. 如果 Desc.[[Value]] 存在,则
        1. 调用 map 对象的内部方法 [[Put]],传入 P、Desc.[[Value]] 和 Throw 作为参数。
      2. 如果 Desc.[[Writable]] 存在,且其值为 false,则
        1. 调用 map 对象的内部方法 [[Delete]],传入 P 和 false 作为参数。
  6. 返回 true

调用 arguments.[[DefineOwnProperty]](P, Desc, Throw) (= 运算符和 Object.defineProperty 会调用 [[DefineOwnProperty]]),会按照上面的步骤执行。

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]] 的值。

  2. 令 isMapped 为 map.[[GetOwnProperty]](P) 的执行结果。

  3. 令 allowed 为 arguments.[[DefineOwnProperty]](P, Desc, false)

    这里的 [[DefineOwnProperty]] 是普通对象的内部方法的算法。

  4. 若 allowed 为 false,如果 Throw 是 true,则抛出 TypeError,否则返回 false。

  5. 若 allowed 不是false,且 isMapped 的值不为 undefined,则

    1. 如果 Desc 不为空和 Desc.[[Get]] 或者 Desc.[[Set]] 存在,则 IsAccessorDescriptor(Desc) 返回 true,则执行下面步骤。

      1. 调用 map. [[Delete]](P, false)。

        var b = 10;
        
        function foo(a) {
        	console.log(a); // 100
        	
        	Object.defineProperty(arguments, '0', {
        		get: function () {
        			return b;
        		},
        		set: function (val) {
        			b = val;
        		}
        	});
        	
        	arguments[0] = 20;
        	console.log(a); // 100
        	console.log(arguments[0]); // 20
        }
        
        foo(100);
        

        调用 map.[[Delete]](p, false) 之后,arguments 对象和形参没有任何关系。

    2. 如果 IsAccessorDescriptor(Desc) 返回 false

      1. 存在 Desc.[[Value]],调用 map.[[Put]](P, Desc.[[Value]], Throw),因为在非严格模式下,形参和 arguments是共享值的,所以此时 arguments[indx] 也会修改。

        function foo(a) {
        	console.log(a); // 1
        	arguments[0] = 10
        	console.log(a); // 10
        	conso.e.log(arguments[0]); // 10
        }
        
        foo(1);
        
      2. 存在 Desc.[[Writable]],且其值为 false,调用 map.[[Delete]](P, false)。

[[Delete]]

arguments 对象的内部方法 [[Delete]] 在一个非严格模式下带有形参的函数中,在一个属性名为 P,布尔标志为 Throw 的条件下被调用时,其执行步骤如下:

  1. 令 map 为 arguments 对象的内部属性 [[ParameterMap]] 的值。
  2. 令 isMapped 为 map 对象的内部方法 [[GetOwnPropery]] 传入参数 P 的执行结果。
  3. 令 result 为 arguments 对象的内部方法 [[Delete]]8.12.7)传入参数 P 和 Throw 的执行结果。
  4. 如果 result 为 true,且 isMapped 不为 undefined,则
    1. 调用 map 对象的内部方法 [[Delete]],传入 P 和 false 作为参数。
  5. 返回 result。

在非严格模式下,

调用 arguments.[[Delete]](P) (这里的 [[Delete]] 是普通对象的 [[Delete]]),结果置为 result。

如果 result 为 true,并且 map 对象有 P 属性,则调用 map.[[Delete]](P, false)。

返回 result。

function foo(a) {
	delete arguments[0];
	arguments[0] = 10;
	console.log(a); // 1
	console.log(arguments[0]); // 10
}

foo(1);

小结

我们知道在非严格模式下,实参和 arguments[indx] 是共享值的。但是只要调用 map.[[Delete]](P, false) 之后,这种关系将被打破。有以下三种情况会调用 map.[[Delete]](P, false):

  • Object.defineProperty(obj, 'indx', Desc) 的属性描述符 Desc 为 undefined,Desc.[[Get]] 或者 Desc.[[Set]] 存在
  • Object.defineProperty(obj, 'indx', Desc) 的属性描述符中 Desc.[[Writable]] 存在且值为 false
  • delete arguments[indx]

构造函数

通过 new 运算符调用的函数都可称为构造函数。

new 运算符

产生式 NewExpression : new NewExpression 按照下面的过程执行:

  1. 令 ref 为解释执行 NewExpression 的结果。
  2. 令 constructor 为 GetValue(ref)。
  3. 如果 Type(constructor) 不是 Object,抛出一个 TypeError 异常。
  4. 如果 constructor 没有实现 [[Construct]] 内置方法,抛出一个 TypeError 异常。
  5. 返回调用 constructor 的 [[Construct]] 内置方法的结果,传入按无参数传入参数列表(就是一个空的参数列表)。

产生式 MemberExpression : new MemberExpression Arguments 按照下面的过程执行 :

  1. 令 ref 为解释执行 MemberExpression 的结果。
  2. 令 constructor 为 GetValue(ref)。
  3. 令 argList 为解释执行 Arguments 的结果,是一个参数值的内部列表(11.2.4)。
  4. 如果 Type(constructor) 不是 Object,抛出一个 TypeError 异常。
  5. 如果 constructor 没有实现 [[Construct]] 内置方法,抛出一个 TypeError 异常。
  6. 返回以 argList 为参数调用 constructor 的 [[Construct]] 内置方法的结果。

new 运算符有两种调用方式 new Foonew Foo(params) ,这两种方式除了有无参数其他都一样。这里以有参数为例:

首先令 constructor 为 GetValue(ref) 。constructor 就是函数对象(构造函数),令 argList 为参数列表。函数对象在创建的时候会内置 [[Construct]] 方法。

new 运算符结果就是最后一点所描述的结果:调用 constructor(函数对象)的 [[Construct]]] 内置方法的结果。

[[Construct]]

当以一个可能的空的参数列表调用函数对象 F 的 [[Construct]] 内部方法,采用以下步骤:

  1. 令 obj 为新创建的 ECMAScript 原生对象。
  2. 依照 8.12 设定 obj 的所有内部方法。
  3. 设定 obj 的 [[Class]] 内部属性为 "Object"
  4. 设定 obj 的 [[Extensible]] 内部属性为 true
  5. 令 proto 为以参数 "prototype" 调用 F 的 [[Get]] 内部属性的值。
  6. 如果 Type(proto) 是 Object,设定 obj 的 [[Prototype]] 内部属性为 proto。
  7. 如果 Type(proto) 不是 Object,设定 obj 的 [[Prototype]] 内部属性为 15.2.4 描述的标准内置的 Object 原型对象。
  8. 以 obj 为 this 值,调用 [[Construct]] 的参数列表为 args,调用 F 的 [[Call]] 内部属性,令 result 为调用结果。
  9. 如果 Type(result) 是 Object,则返回 result。
  10. 返回 obj。

前七点,创建一个 ECMAScript 原生对象 obj,并添加内部属性。

第八点,以 obj 为 this 值,args 为参数列表,调用 F 的 [[Call]] 内部属性(也就是普通的函数调用),result 为调用结果。

第八九点,根据 result 判断返回值:如果 result 是对象,返回 result,否则返回刚才新建对象 obj。

function Foo(a) {
	this.a = a;
	return 1;
}

var res = new Foo(1);
console.log(res); // Foo {a: 1}

因为 Foo 函数返回值是数字 1 ,不是对象,所以 new Foo(1) 返回新建的对象。

function Foo(a) {
	this.a = a;
	return {
		b: 10
	};
}

var res = new Foo(1);
console.log(res); // {b: 10}

Foo 函数返回值是一个对象 {b: 10},所以 new Foo(1) 返回 {b: 10}

函数扩展:函数语句

JavaScript 语句不包括函数声明(FunctionDeclaration),函数声明不能作为语句使用。在 [ECMAScript 语句](ECMAScript 语句) 的注释中有明确的提示说明:

注: 已知几个广泛使用的 ECMAScript 实现支持 FunctionDeclaration 当作语句使用。然而,在实现之间这种 FunctionDeclaration 应用的语义也有严重且不兼容的差异。由于这些不兼容的差异,将 FunctionDeclaration 当作 Statement 使用的结果是代码在实现之间的可移植性不可靠。建议 ECMAScript 实现禁止这样运用 FunctionDeclaration,或遇到这样的运用是发出一个警告。ECMAScript 的未来版本可能定义替代的兼容方案以在 Statement 上下文中声明函数。

不过实际实现中,允许函数声明作为语句使用,我们常见的,比如在 if、for 中函数声明都作为语句使用

console.log(foo); // undefined

if (true) {
	function foo() {
		console.log('foo');
	}
}

foo(); // foo

函数声明 foo 被当做语句使用,foo 相当于变量了。在执行 if 语句之后,foo 才被赋值为函数对象。

有一点 需要注意,foo 标识符,在执行环境中创建的不可变绑定。

if (true) {
	functtion foo() {}
	
	var foo = 1;
}

// SyntaxError: Identifier 'foo' has already been declared

这一点在各大浏览器下实现的都一致。

参考资料

ECMAScript 英文文档

ECMAScript 维基百科 中文文档

函数 (Functions)深入理解JavaScript系列(15):函数 (Functions)

MDN 函数

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant