8000 原型与原型链详解 · Issue #16 · tiger5wang/blog · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

原型与原型链详解 #16

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

Open
tiger5wang opened this issue Dec 10, 2019 · 0 comments
Open

原型与原型链详解 #16

tiger5wang opened this issue Dec 10, 2019 · 0 comments

Comments

@tiger5wang
Copy link
Owner
tiger5wang commented Dec 10, 2019

在说原型 和 原型链 之前,需要先弄清楚对象的分类及区别

一、对象的分类:普通对象 与 函数对象

在JS 中,万物皆对象,但是对象也是有区别的,分别为 普通对象函数对象,Object, Function, Array, String, Number, Boolean, Date 等都是 JS 内置的函数对象。举例说明

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object);       //function 
console.log(typeof Function);   //function  

console.log(typeof f1);       //function 
console.log(typeof f2);       //function 
console.log(typeof f3);      //function   

console.log(typeof o1);     //object 
console.log(typeof o2);     //object 
console.log(typeof o3);     //object

在上面的例子中 o1, o2,o3 是普通对象,f1,f2,f3 是函数对象。那么怎么来区分他们呢? 是这样的:凡是通过 new Function() 创建的对象都是函数对象,其他的对象都是普通对象注意 f1, f2 归根到底也是通过 new Function() 创建的,上面的写法只是一个语法糖,所以他们也是函数对象,也可以说任意一个函数都是函数对象,函数对象就是函数。另外,Object, Function, Array, String, Number, Boolean, Date 这些 JS 内置函数对象也是通过 new Function() 创建的。

一定要分清楚普通对象和函数对象,这样利于下面的理解。

二、构造函数

先想一下基本的构造函数的内容:

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function() { alert(this.name) } 
}
var person1 = new Person('Zaxlct', 28, 'Software Engineer');
var person2 = new Person('Mick', 23, 'Doctor');

在这个例子中,person1, person2 都是 Person 的实例,这两个实例都有一个 constructor(构造函数)属性,该属性(是一个指针)指向 他们的构造函数 Person, 即:

console.log(person1.constructor === Person);   //true
console.log(person2.constructor === Person);   //true

这里要记住两个概念: 构造函数,实例
person1 和 person2 都是构造函数 Person 的实例

以及最重要的一点:实例 的 构造函数属性(constructor)指向 构造函数本身

在 JS 中,构造函数本身首先是一个函数,可以说任意的一个函数 都可以当作构造函数,但是一般在定义自定义构造函数时,要将函数名首字母改用大写的形式,以跟普通函数区分,形式就是上面的 Person 构造函数的形式。
另外,JS 的内置函数 Object, Function, Array, String, Number, Boolean, Date 等都是构造函数,跟 Person 差不多,只不过他们只是 JS 内部 出于创建 特定类型的 对象 而自定义的,他们又叫构造器,都是函数对象。

typeof Object
"function"
typeof Function
"function"
typeof Array
"function"
typeof String
"function"
typeof Number
"function"
typeof Boolean
"function"
typeof Date
"function"

三、原型对象

在 JS 中,每当定义一个对象(函数也是对象),对象中都会包含一些预定义的属性。其中每个 函数 (对象) 都会有一个 prototype 属性(是一个指针),它指向 一个对象-- 通过该函数创建的实例对象的原型,所以叫原型对象。在上面的例子中就是 person1, person2 的原型。

那什么是原型呢?可以这样理解:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联一个对象,这个对象就是我们所说的原型,它包含了特定类型的所有实例共享的属性和方法,每一个对象都会从原型继承这些属性、方法。所以,原型对象的好处是:可以让所有的实例对象共享它所包含的属性和方法。当然,我们也可以对这个对象进行扩展,比如:

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

var person1 = new Person();
person1.sayName(); // 'Zaxlct'

var person2 = new Person();
person2.sayName(); // 'Zaxlct'

console.log(person1.sayName == person2.sayName); //true

Person.prototype 就是原型对象,上面的 Person.prototype.xxx 这些操作是 我们自己 对原型对象进行的 扩展,目的就是 Person 构造函数的所有实例都可以 继承 这些属性。

默认情况下,所有的 原型对象 都会包含一个 constructor (构造函数)属性(一个指针),这个属性)指向 prototype 属性所在的 构造函数。

用到上面的例子就是: Person.prototype.constructor === Person.

在第二部分中有:person1.constructor === Person。

他们好像有点联系:

Person.prototype.constructor === Person
person1.constructor === Person

在第二部分,我们知道 实例的构造函数属性(constructor)指向构造函数,person1 有 constructor 属性,是因为 person1 是 Person 的实例。

那么,Person.prototype 为什么也有 constructor 属性呢?结合下面的信息:

typeof Person.prototype
"object"

我们可以得出: Person.prototype 也是 Person 的实例对象。也就是说:

原型对象(Person.prototype)就是 构造函数(Person) 的一个实例对象,相当于在构造函数创建的时候,自动创建了一个它的实例,并它这个实例赋值给了它的(构造函数) prototype 属性,基本过程大致如下:

function Person(){};
var temp = new Person();
person.prototype = temp;
 
function Function(){};
var temp = new Function();
Function.prototype = temp;

从而可以知道,原型对象 其实就是 普通对象。但是,注意一个例外:Function.prototype ,它是函数对象,但它也很特殊,没有 prototype 属性(前面说了所有的函数对象都有 prototype 属性)。看例子:

function Person(){};
console.log(Person.prototype) //Person{}
console.log(typeof Person.prototype) //Object
console.log(typeof Function.prototype) // Function,这个特殊
console.log(typeof Object.prototype) // Object
console.log(typeof Function.prototype.prototype) //undefined

Function.prototype 为什么会是函数对象呢?

function Function(){};
var temp = new Function();
Function.prototype = temp;

因为:原型对象是构造函数的一个实例,而这个实例是通过 new Function() 方式创建的,第一部分 提到了:凡是通过 new Function() 方式创建的对象都是函数对象,所以Function.prototype 是函数对象。

那么,原型对象 的主要作用是什么呢,存在什么问题吗?

原型对象的主要作用是 :用于继承,对特定类型预定义一些相关的属性和方法,让所有的实例都可以使用 原型对象包含的属性和方法。这个共享的特性有很多的好处,同时也存在一个缺点:

原型对象的最大的问题就是由其共享的本性导致的。某一个实例对象比较特殊,需要修改它的原型对象的某个属性,如果修改了原型对象,那么所有的实例的这个属性都会跟着修改,。

function Person(){
}
Person.prototype = {
	constructor: Person,
    name : "snake",
    age : "18",
    sayName : function(){
    	console.log(this.name);
    },
    friends : ["f1","f2"] //***数组类型
};
 
var person1 = new Person();
var person2 = new Person();

person1.age =12; // 此方法是给实例 person1 添加了一个 age 属性,不是修改的原型对象的属性
console.log(person1.age); // 12 访问实例属性时,优先访问实例本身的属性
console.log(person2.age); // 18 实例本身没有此属性,则像原型对象中查找

person1.friends = [ 'f3', 'f1', 'f2' ]  // 同上,也是直接给实例 person1 添加一个属性 friends,不是修改原型对象的属性
console.log(person1.friends)  // ["f3", "f1", "f2"] 访问实例属性时,优先访问实例本身的属性
console.log(person2.friends)  // ["f1", "f2"]  实例本身没有此属性,则像原型对象中查找

// 注意下面代码和上面的不同之处,不能和上面的代码同时执行哦,要执行下面的代码就不能执行上面这段代码,否则结果就不一样了(因为修改的就不是原型对象了)
person1.friends.unshift("f3"); // 因为实例对象 person1 没有 friends 属性,所以此处修改的是它的原型对象的属性
// 两个实例访问的都是 原型对象的属性,所以值一样,指向的地址也一样
console.log(person1.friends); // [ 'f3', 'f1', 'f2' ] 
console.log(person2.friends); // [ 'f3', 'f1', 'f2' ]
console.log(person1.friends == person2.friends); //true

有上述例子可以看出,当一个实例对象 修改或赋值 一个属性时,如果此对象本身没有此属性,就会给对象加上此属性,如果有就会修改此属性,修改了实例的属性不会影响其他实例的属性;如果修改了原型对象的属性,则所有的实例访问此属性时都会跟着修改。

如何解决这个问题呢?可以组合使用 构造函数模式 和 原型模式,将值不同的属性放在构造函数中,而不是原型对象中。

function Person(){
    this.family = ["father","mother"]; // 构造函数中定义一属性,创建实例对象时会添加到实例的属性上
}
Person.prototype = {
	constructor: Person,
    name : "snake",
    age : "18",
    sayName : function(){
    	console.log(this.name);
    },
    
};
 
var person1 = new Person();
var person2 = new Person();

person1.family.pop(); //改变构造函数中定义的引用类型属性
console.log(person1.family); // [ 'father' ]
console.log(person2.family); // [ 'father', 'mother' ]
console.log(person1.name); // snake
console.log(person2.name); //snake

还有一种问题就是:一个构造函数的多个实例中,他们使用 原型对象中的某一个方法返回值是一样的,只有一个特殊的实例使用这个方法时需要其他的返回值,甚至多个实例都有一个方法与其他实例不通过,需要怎么处理呢?

我的想法是使用默认参数

这里需要注意的是:当以参数列表形式设置默认值时,赋值方式是按序赋值的,因为多数实例都使用默认参数,有特例属性的地方才需要传值,所以这里传值时要使用对象,然后解构取值

function Person({hello='你好', eat='吃饭'}){
    this.hello = hello
    this.eat = eat
}
Person.prototype = {
	constructor: Person,
	sayHello: function() {
		console.log(this.hello)
	},
	syaEat: function() {
		console.log(this.eat)
	}
}
var p1 = new Person({})
var p2 = new Person({})
var p3 = new Person({eat:'eat food'})
var p4 = new Person({hello:'hi'})

p1.sayHello()  // 你好
p2.sayHello()  // 你好
p3.sayHello()  // 你好
p4.sayHello()  // hi

p1.syaEat()  // 吃饭
p2.syaEat()  // 吃饭
p3.syaEat()  // eat food
p4.syaEat()  // 吃饭

原型对象还有一个作用:就是减少内存占用。在 JS 中,除了 原型对象 的属性和方法之外其他的属性方法都是独立的。就是说, 如果复创建了多个对象,那么每个对象中的方法都会在内存中开辟新的空间,这样浪费的空间就比较多。

function Person(name, age) {   
  this.name = name;
  this.age = age;
  this.say = function () {
    return this.name  + ',' + this.age;;
}
var p1= new Person('a', 25);
var p2= new Person('b', 22); 
alert(p1.say == p2.say);//结果返回的是 false, 说明不同实例对象的此方法在内存空间中指向不同的地址,即分别开辟了内存空间保存他们的方法

而原型对象中的属性方法就不一样了,因为原型对象是所有实例共享的。所有的实 8000 对象都共享这个原型对象,而不会开辟新的空间。

function User(name,age){
    this.name = name;
    this.age = age;
}
User.prototype.addr = 'shenzhen';//在原型中添加属性
User.prototype.show = function(){//在原型中添加方法
    alert(this.name+'|'+this.age);    
};
var user1 = new User('ZXC',22);
var user2 = new User('CXZ',21);
alert(user1.show == user2.show);//返回 true 说明show方法是共享的,即原型对象中的方法,所有的实例共用一个

image-20191211154302819

四、 __ proto __

JS 在创建对象(不论是普通对象还是函数对象)后,实例都会有一个叫做 __ proto __ 的属性(内部指针),指向创建它的构造函数的原型对象。

在 Person 的例子中就是:person1.__ proto __ === Person.prototype

请看下图:

proto

《JavaScript 高级程序设计》的图 6-1

根据这个图,我们可以知道:

Person.prototype.constructor === Person
person1.__proto__ === Person.prototype
person1.constructor === Person

这里需要明确的一点是,这个连接存在于实例(person1)与 构造函数(Person)的原型对象(Person.prototype)之间,而不是存在于 实例(person1)和 构造函数(Person)之间。

注意: ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__ proto __ , 现在 绝大部分浏览器都支持__ proto __属性,所以它才被加入了 ES6 里(ES5 部分浏览器也支持,但还不是标准)。

实例对象、原型对象、构造函数之间的关系

五、函数对象

所有函数(对象)的 __ proto __ 都指向 Function.prototype,它是一个空函数(Empty function)
console.log(Function.prototype)
ƒ () { [native code] }
Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true

JavaScript中有内置(build-in)构造器/对象共计12个(ES5中新加了JSON),这里列举了可访问的8个构造器。剩下如Global不能直接访问,Arguments仅在函数调用时由JS引擎创建,Math,JSON是以对象形式存在的,无需new。它们的 __ proto __ 是Object.prototype。

Math.__proto__ === Object.prototype  // true
Math.construrctor == Object // true

JSON.__proto__ === Object.prototype  // true
JSON.construrctor == Object //true

上面说的函数对象当然包括自定义的。如下

// 函数声明
function Person() {}
// 函数表达式
var Perosn = function() {}
console.log(Person.__proto__ === Function.prototype) // true
console.log(Man.__proto__ === Function.prototype)    // true

这说明什么呢?

所有的构造器都来自于 Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了 Function.prototype 的属性及方法。如length、call、apply、bind

Function.prototype也是唯一一个typeof XXX.prototype为 function的prototype。其它的构造器的prototype都是一个对象(原因第三节里已经解释过了)。如下(复习一遍):

console.log(typeof Function.prototype) // function
console.log(typeof Object.prototype)   // object
console.log(typeof Number.prototype)   // object
console.log(typeof Boolean.prototype)  // object
console.log(typeof String.prototype)   // object
console.log(typeof Array.prototype)    // object
console.log(typeof RegExp.prototype)   // object
console.log(typeof Error.prototype)    // object
console.log(typeof Date.prototype)     // object
console.log(typeof Object.prototype)   // object

知道了所有构造器(含内置及自定义)的原型 __ proto __ 都是Function.prototype,那Function.prototype的__ proto __ 是谁呢?
相信都听说过JavaScript中函数也是一等公民,那从哪能体现呢?如下
console.log(Function.prototype.__ proto __ === Object.prototype) // true

这说明所有的构造器也同时是一个JS 对象,可以给构造器添加/删除属性等。同时它也继承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。

最后Object.prototype的 __ proto __ 是谁?
Object.prototype.__ proto __ === null // true

已经到顶了,为null。

六、prototype

在 ECMAScript 核心所定义的全部属性中,最耐人寻味的就要数 prototype 属性了。对于 ECMAScript 中的引用类型而言,prototype 是保存着它们所有实例方法的真正所在。换句话所说,诸如 toString()和 valuseOf() 等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访问罢了。

——《JavaScript 高级程序设计》第三版 P116

我们知道 JS 内置了一些方法供我们使用,比如:

对象可以用 constructor/toString()/valueOf() 等方法;
数组可以用 map()/filter()/reducer() 等方法;
数字可用用 parseInt()/parseFloat()等方法;

当我们创建一个对象时:

var Person = new Object()
Person 是 Object 的实例,所以 Person 继承了Object 的原型对象Object.prototype上所有的方法:

Object.prototype
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()

Object 的每个实例都具有以上的属性和方法。
所以我可以用 Person.constructor 也可以用 Person.hasOwnProperty。

当我们创建一个数组时:

var num = new Array()
num 是 Array 的实例,所以 num 继承了Array 的原型对象Array.prototype上所有的方法:

Array.prototype
[constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]

concat: ƒ concat()constructor: ƒ Array()copyWithin: ƒ copyWithin()entries: ƒ entries()every: ƒ every()fill: ƒ fill()filter: ƒ filter()find: ƒ find()findIndex: ƒ findIndex()flat: ƒ flat()flatMap: ƒ flatMap()forEach: ƒ forEach()includes: ƒ includes()indexOf: ƒ indexOf()join: ƒ join()keys: ƒ keys()arguments: (...)caller: (...)length: 0name: "keys"__proto__: ƒ ()[[Scopes]]: Scopes[0]lastIndexOf: ƒ lastIndexOf()length: 0map: ƒ map()pop: ƒ pop()push: ƒ push()reduce: ƒ reduce()reduceRight: ƒ reduceRight()reverse: ƒ reverse()shift: ƒ shift()slice: ƒ slice()some: ƒ some()sort: ƒ sort()splice: ƒ splice()toLocaleString: ƒ toLocaleString()toString: ƒ toString()unshift: ƒ unshift()values: ƒ values()Symbol(Symbol.iterator): ƒ values()Symbol(Symbol.unscopables): {copyWithin: true, entries: true, fill: true, find: true, findIndex: true, …}__proto__: Object

内容多有点乱, 可以用一个 ES5 提供的方法:Object.getOwnPropertyNames , 获取所有**(包括不可枚举的属性)**的属性名, 不包括 prototype 中的属性,返回一个数组:

var arrayAllKeys = Array.prototype; // [] 空数组
// 只得到 arrayAllKeys 这个对象里所有的属性名(不会去找 arrayAllKeys.prototype 中的属性)
console.log(Object.getOwnPropertyNames(arrayAllKeys)); 
/* 输出:
["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", 
"concat", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", 
"some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight", 
"entries", "keys", "copyWithin", "find", "findIndex", "fill"]
*/

所以我们知道了,我们创建的数组为啥能用那么多方法了

但是,Object.getOwnPropertyNames(arrayAllKeys) 输出的数组里并没有 hasOwnProperty 等对象的方法。但是随便定义的数组也能用这些方法:

var num = [1];
console.log(num.hasOwnProperty ()) // false (输出布尔值而不是报错)

为什么?因为Array.prototype 虽然没这些方法,但是它有原型对象(__ proto __):

// 上面我们说了 Array.prototype 就是一个普通对象。
Array.prototype.__proto__ == Object.prototype

所以 Array.prototype 继承了对象的所有方法,当你用num.hasOwnProperty()时,JS 会先查一下它的构造函数 (Array) 的原型对象 Array.prototype 有没有有hasOwnProperty()方法,没查到的话继续查一下 Array.prototype 的原型对象 Array.prototype.__ proto __有没有这个方法。

当我们创建一个函数时:

var f = new Function("x","return x*x;");
//当然你也可以这么创建 f = function(x){ return x*x }
console.log(f.arguments) // arguments 方法从哪里来的?
console.log(f.call(window, 2)) // call 方法从哪里来的?
console.log(Function.prototype) // function() {} (一个空的函数)
console.log(Object.getOwnPropertyNames(Function.prototype)); 
/* 输出
["length", "name", "arguments", "caller", "constructor", "bind", "toString", "call", "apply"]
*/

所有函数 (对象) 的 __ proto __ 属性都指向 Function.prototype,它是一个空函数(Empty function)。

七、原型链

先看几个问题

person1.__proto__ === Person.prototype     // true
Person.prototype.__proto__ === Object.prototype    // true
person1.__proto__.__proto__ === Object.prototype   // true
Object.prototype.__proto__ === null     // true
function Person() {

}

Person.prototype.name = 'Kevin';

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin

从上面的例子中可以得出:

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性(原型对象),如果还查不到,就去找原型的原型,一直找到最顶层为止,最终的结果就是 null, 表示没有对象,即该处不应该有值。**在这个查找过程中,由相互关联的原型组成的链状结构就构成了原型链。**注意这个链状结构是由 __ proto __ 连接起来的。

还有点疑惑:

Person.__proto__ === Function.prototype    // true
Function.prototype.__proto__ === Object.prototype   // true
Person.__proto__.__proto__ === Object.prototype     // true

Object.__proto__ === Function.prototype    // true
Object.__proto__.__proto__ === Object.prototype    // true

Function.__proto__ === Function.prototype    // true
  1. Object.__ proto __ === Function.prototype // true

    Object 是函数对象,是通过 new Function() 创建的,所以 Object.__ proto __ 指向 Function.prototype。

  2. Function.__ proto __ === Function.prototype // true

    Function 也是函数对象,也是通过 new Function() 创建的,所以,Function.__ proto __ 指向 Function.prototype。

    自己由自己创建的,好像不符合逻辑,但仔细想想,显示世界也有些类似。追根溯源,我们的宇宙是怎么来的呢,就是无。

    也正如《道德经》里说的“无,名天地之始”。

  3. Function.prototype.__ proto __ === Object.prototype // true

    对于这一点,也是非常困惑,不知道下面的说法可否。

    Function.prototype 本身是函数对象(空函数),理论上它的 __ proto __ 属性应该指向 Function.prototype, 这样就成了自己指向自己,自己指向自己,没有意义,而且可能造成死循环。

    JS 也强调万物皆对象,函数对象也是对象,所以给他认个祖宗,指向 Object.prototype.

    Object.prototype.__ proto __ === null,保证了原型能够正常结束。

Object、Function之间的联系

八、原型继承

原型链是实现继承的主要方法 。

基本思路:

利用原型让一个引用类型继承另一个引用类型的属性和方法。

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数想指针(constructor),而实例对象都包含一个指向原型对象的内部指针( __ proto __ )。如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针( __ proto __ ),另一个原型也包含着一个指向另一个构造函数的指针(constructor)。假如另一个原型又是另一个类型的实例……这就构成了实例与原型的链条。

原型链继承

@tiger5wang tiger5wang changed the title 原型与原型链 原型与原型链详解 Dec 10, 2019
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
0