何米酥`s Blog

js高程读书笔记 第五章 引用类型

August 04, 2017

本章内容

  • 使用对象
  • 创建并操作数组
  • 理解基本的javasrcipt类型
  • 使用基本类型和基本包装类型

引用类型是一种数据结构,用于将数据和功能组织在一起。 对象是某个特定引用类型的实例。

object类型

创建object实例的方式有两种。

  1. 用new操作符后跟object构造函数
var person = new Object();
person.name = "Hemisu";
person.age = 24;
  1. 对象字面量表示法
var person = {
    name : 'nicholas',
    age : 24
}

访问对象属性可以用点表示法和方括号表示法来访问对象的属性。

alert(person["name"]);
alert(person.name);

方括号语法可以通过变量来访问属性

var propertyName = "name";
alert(pserson[propertyName]);

推荐使用点表示法

Array类型

ECMAScript数组的每一项可以保存任何类型的数据。 创建数组的基本方法有两种。

  1. 使用Array构造函数。
var colors = new Array();
  1. 数组字面量表示法。
var colors = ["red", "blue", "green"];
var names = [];//空数组

数组的项数保存在lenth属性中,这个属性始终会返回0或者更大的值,可以通过设置这个属性,从数组的末尾移除项或向数组中添加新项。

检测数组

使用instanceof操作符:

if (value instanceof Array){
    //对数组进行操作
}

问题在于,它假定只有一个全局执行环境。如果网页种包含多个框架,实际上就存在两个以上不同的全局执行环境。从而存在两个以上不同版本的Array构造函数。

为了解决这个问题,ECMAScript 5新增Array.isArray()方法。

if (Array.isArray(value)){
    //对数组执行某些操作
}

转换方法

所有对象都具有toLocalString()、toString()和valueOf()方法。

数组继承的toLocalString()、toString()和valueOf()方法,在默认情况下都会以逗号分隔的字符串形式返回数组项。用join()方法可以使用不同的分隔符来构建这个字符串。

var colors = ["red", "green", "blue"];
alert(colors.join(","));
alert(colors.join("||"));

如果数组中某项的值是null或者undefined,那么该值在toLocalString()、toString()、valueOf()和join()方法返回的结果中以空字符串表示。

栈方法

push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。 pop()方法则从数组末尾移除最后一项,减少数组的lenth值,然后返回移除的项。

var colors = new Array();       //创建一个数组
var count = colors.push("red", "green");//推入两项
alert(count);                   //2

count = colors.push("black");
alert(count);

var item = colors.pop();        //取得最后一项
alert(item);                    //black
alert(color.length);            //2

队列方法

var colors = new Array();       //创建一个数组
var count = colors.push("red", "green");//推入两项
alert(count);                   //2

count = colors.push("black");
alert(count);

var item = color.shift();       //取得第一项
alert(item);                    //red
alert(colors.length);           //2

unshift()的用途与shift()的用途相反,它能在数组前端添加任意个项并返回新数组的长度。

重排序方法

数组中存在两个可以用来重排序的方法:reverse()和sort()

如果直接使用sort来排序,sort会调用每个数组项的toString()转型,比较的是字符串。于是会出现如下情况:

var values = [0, 1, 5, 10, 15];
values.sort();
alert(values);//0,1,10,15,5

为了解决这种情况,sort()方法可以接收一个比较函数,一边指定哪个值位于哪个值的前面。

function compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0;
    }
}
var values = [0, 1, 5 , 10, 15];
values.sort(compare);
alert(values);  //0,1,5,10,15

操作方法

concat()方法可以基于当前数组种的所有项创建一个新数组。这方法会先创建当前数组的一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。

var colors = ["red", "green", "blue"];
var colors2 = colors.concat("yello", ["black", "brown"]);

alert(colors);      //red,green,blue
alert(colors2);     //red,green,blue,yellow,black,brown

slice()方法能够基于当前数组种的一个或多个热项创建一个新书组。slice()方法可以接收一或两个参数,即要返回项的起始和结束位置。slice方法不会影响原始数组。

var colors = ["red", "green", "blue", "yellow", "purple"];
var colors2 = colors.slice(1);
var colors3 = colors.slice(1,4);

alert(colors2);     //green,blue,yellow,purple
alert(colors3);
//green,blue,yellow

如果slice()方法的参数种有一个负数,则用数组长度加上该数来确定相应的位置。长度5的数组调用slice(-2,-1)等于slice(3,4)。

splice()方法

  • 删除

可以删除任意数量的项,只需指定2个参数:要删除的第一项的位置和要删除的项数。

  • 插入

可以向指定位置插入任意数量的项,只需提供3个参数:起始位置、0(要删除的项)和要插入的项。如果要插入多个项,可以再传入第四、第五以致任意多项。

  • 替换

可以同时指定删除和替换。

var colors = ["red", "green", "blue"];
var removed = colors.splice(0,1);
alert(colors);//green,blue
alert(removed);//red

removed = colors.splice(1,0,"yellow", "orange");
alert(colors);//green,yellow,orange,blue
alert(removed);//[]

removed = colors.splice(1,1,"red", "purple");
alert(colors);//green,red,purple,orange,blue
alert(removed);//yellow

位置方法

indexOf()和lastIndexOf()

接收两个参数:要查找的项和(可选的)表示查找起点的位置。

var numbers=[1,2,3,4,5,4,3,2,1];
alert(numbers.indexOf(4));//3
alert(numbers.lastIndexOf(4));//5

alert(numbers.indexOf(4, 4));//5,从4号位置开始找4
alert(numbers.lastIndexOf(4, 4));//3

var person = { name: "Nicholas" };
var people = [{ name: "Nicholas"}];

var morePeople = [person];

alert(people.indexOf(person));//-1
alert(morePeople.indexOf(person));//0

迭代方法

ECMAScript为数组定义了5个迭代方法。每个方法都会接收两个参数:要在每一项上运行的函数和(可选)运行该函数的作用域对象——影响this的值。

传入这些方法中的函数会接收三个参数: 数组项的值、该项在数组中的位置和数组对象本身。

  • every(): 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true。
  • filter(): 对数组中的每一项运行给定函数,返回该函数会返回的true的项组成的数组。
  • forEach(): 对数组中的每一项运行给定函数。没有返回值
  • map(): 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
  • some(): 对数组中的每一项运行给定函数,如果函数对任一项返回true,则返回true。
var numbers = [1,2,3,4,5,4,3,2,1];
var everyResult = numbers.every(function(item, index, array){
    return (item > 2);
});
alert(everyResult);//false

var someResult = numbers.every(function(item, index, array){
    return (item > 2);
});
alert(someResult);//true

var filterResult = numbers.every(function(item, index, array){
    return (item > 2);
});
alert(filterResult);//[3,4,5,4,3]

var mapResult = numbers.every(function(item, index, array){
    return (item * 2);
});
alert(mapResult);//[2,4,6,8,10,8,6,4,2]

归并方法

reduce()和reduceRight()

接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给reduce()和reduceRight()的函数接收4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第二个参数自动传给下一项。

var values = [1,2,3,4,5];
var sum = values.reduce(function(prev, cur, index, array){
    return prev + cur;
});
alert(sum);//15

Date类型

创建一个日期对象,使用new操作符和Date构造函数。

var now = new Date();

Date.parse()方法接收一个表示日期的字符串参数。

Date.UTC()的参数分别是年、基于0的月份,天,小时,分钟,秒及毫秒。

Date()构造函数模仿Date.UTC(),区别是Date()基于本地时区而非GMT来创建。

继承方法

tiKicakString()、toString()和valueOf()

日期格式化方法

推荐使用toUTCString()方法

RegExp类型

ECMAScript通过RegExp类型来支持正则表达式。

var expression = / pattern / flags;

flags:

  • g 表示全局模式,应用于所有字符串,而非在发现第一个匹配项时立即停止;
  • i 表示不区分大小写(case-insensitive)模式
  • m 表示多行模式,即在到达一行文本末尾时还会继续查找下一行种是否存在与模式匹配的值
/*
 * 匹配字符串种所有"at"的实例
 */
var pattern1 = /at/g;

/*
 * 匹配第一个"bat"或"cat",不区分大小写
 */
var pattern2 = /[bc]cat/i;
/*
 * 匹配所有以"at"结尾的3个字符的组合,不区分大小写
 */
var pattern3 = /.at/gi;

RegExp实例属性

  • global:布尔值,表示是否设置了g标志
  • ignoreCase:布尔值,表示是否设置了i标志
  • lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从0算起
  • multiline:布尔值,表示是否设置了m标志。
  • source:正则表达式的字符串表示。

RegExp实例方法

exec()接收一个参数,即要应用模式的字符串,饭后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回null。

对于exec()方法而言,即使在模式中设置了g,每次也只会返回一个匹配项。如果设置了g,可以多次调用exec()。

var text = "cat, bat, sat, fat";
var pattern1 = /.at/;

var matches = pattern1.exec(text);
alert(matches.index);   //0
alert(matches[0]);      //cat
alert(matches.lastIndex);//0

var matches = pattern1.exec(text);
alert(matches.index);   //0
alert(matches[0]);      //cat
alert(matches.lastIndex);//0

var pattern2 = /.at/g;

var matches = pattern2.exec(text);
alert(matches.index);   //0
alert(matches[0]);      //cat
alert(matches.lastIndex);//3

var matches = pattern2.exec(text);
alert(matches.index);   //5
alert(matches[0]);      //bat
alert(matches.lastIndex);//8

第二个方法是test(),接收一个字符串参数。在模式与该参数匹配的情况下返回true。

var text = "000-00-0000";
var pattern = /\d{3}-\d{2}-\d{4}/;

if(pattern.test(text)){
    alert("这个模式匹配");
}

RegExp构造函数属性

长属性名 短属性名 说明
input $_ 最近一次要匹配的字符串
lastMatch $& 最近一次的匹配项
lastParen $+ 最近一次匹配的捕获组
leftContext $` input字符串中lastMatch之前的文本
multiline $* 布尔值,表示是否所有表达式都是用多行模式。
rightContext $’ Input字符串种lastMatch之后的文本
var text = "this has been a short summer";
var pattern = /(.)hort/g;

if(pattern.text(text)){
    alert(RegExp.input);        //this has been a short summer
    alert(RegExp.leftContext);  //this has been a
    alert(RegExp.rightContext); //summer
    alert(RegExp.lastMatch);    //short
    alert(RegExp.lastParen);    //s
    alert(RegExp.multiline);    //false
}

Function类型

每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。

函数是对象,函数名是指针

function sum(num1, num2) {
    return num1 + num2;
}

var sum = function(num1, num2){
    return num1 + num2;
}

var sum = new Function("num1", "num2", "return num1 + num2");

//这三者定义函数的方式一样

第三种方法不推荐使用,会导致解析两次代码。

没有重载

由于变量名类似指针,所以ECMAScript中没有函数重载的概念。

函数声明和函数表达式

  • 函数声明会被解析器率先读取,在执行任何代码之前可用
  • 函数表达式必须等到解析器执行到它所在的代码行才回真正被执行。

函数声明提升(function declaration hoisting)会读取并将函数声明添加到执行环境中。

作为值的函数

函数可以作为值来使用。

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        
        if(value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    }
}

var data = [{name: "Zachary", age: 28},{name: "Nicholas", age: 29}];
data.sort(createComparisonFunction("name"));
alert(data[0].name);    //Nicholas

data.sort(createComparisonFunction("age"));
alert(date[0].name);    //Zachary

函数内部属性

  • arguments

包含着传入函数中的所有属性这个对象还拥有一个callee的属性,指针指向arguments对象的函数

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}
//这个函数的执行与函数名factorial耦合在一起
//可以使用arguments.callee来消除耦合
function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}
  • this

引用的是函数据以执行的环境对象。

window.color = "red";
var o = {color: "blue"};

function sayColor(){
    alert(this.color);
}

sayColor();     //"red"

o.sayColor = sayColor;
o.sayColor();   //"blue"
  • caller

保存这调用当前属性函数的引用

function outer(){
    inner();
}

function inner(){
    alert(inner.caller);
    //也可以使用arguments.callee.caller
}

outer();

函数属性和方法

属性

ECMAScript中函数是对象,函数有属性和方法。

  • lenth属性表示函数希望接收的命名参数的个数
function sayName(name){
    alert(name);
}

function sum(num1, num2){
    return num1 + num2;
}

function sayHi(){
    alert("Hi");
}

alert(sayName.length);      //1
alert(sum.length);          //2
alert(sayHi.length);        //0
  • prototype属性是保存所有实例方法的真正所在。在ECMAScript5中,prototype属性是不可枚举的,无法使用for-in发现。

方法

apply()和call()。两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。

function sum(num1, num2){
    return num1 + num2;
}

function callSum1(num1, num2){
    return sum.apply(this, arguments);
}

function callSum2(num1, num2){
    return sum.apply(this, [num1, num2]);
}

alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20

call()方法与apply方法的作用相同,区别仅在于接收参数的方式不同。 在使用call()方法时,传递给函数的参数必须逐个列举出来。

function sum(num1, num2){
    return num1 + num2;
}

function callSum(num1, num2){
    return call(this, num1, num2);
}

alert(callSum(10,10));  //20

apply()与call()的真正作用是扩充函数赖以运行的作用域。

window.color = "red";
var o = {color: "blue"};

function sayColor(){
    alert(this.color);
}

sayColor();             //red
sayColor.call(this);    //red
sayColor.call(window);  //red
sayColor.call(o);       //blue

ECMAScript5还定义了一个bind()方法,这个方法会创建一个函数的实例,this值会被绑定到传给bind()函数的值。

window.color = "red";
var o = { color: "blue" };

function sayColor(){
    alert(this.color);
}

var objectSayColor = sayColor.bind(o);
objectSayColor();//blue

基本包装类型

ECMAString提供了3个特殊的引用类型:Boolean、Number、String。

var s1 = "some text";
var s2 = s1.substring(2);

当第二行代码访问s1时,访问过程处于一种读取模式,也就是要从内存中读取这个字符串的值,后台都会自动完成下列处理。

  1. 创建string类型的一个实例
  2. 在实例上调用指定的方法
  3. 销毁这个实例

以上三个步骤想象成执行了下列ECMAString代码

var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;

此番处理,基本的字符串值就变得跟对象一样了。

引用类型与基本包装类型的主要区别就是对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存种。而自动创建的基本包装类型的对象,则只存在于一行代码执行的瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型添加属性和方法。

var s1 = "some text";
s1.color = "red";
alert(s1.color);    //undefined

用new调用基本包装类型的构造函数,与调用同名的转型函数是不一样的。

var value = "25";
var number = Number(value);     //转型函数
alert(typeof number);           //number

var obj = new Number(value);    //构造函数
alert(typeof obj);              //object

Boolean类型

Boolean对象在ECMAScript中用处不大,需要注意的是,布尔表达式种的所有对象都会被转化为true,于是会有下列情况出现。

var falseObject = new Boolean(false);
var result = falseObject && true;
alert(result);  //true

var falseValue = false;
var result = falseValue && true;
alert(result);  //false

alert(typeof falseObject);  //object
alert(typeof falseValue);   //boolean
alert(falseObject instanceof Boolean);  //true
alert(falseValue instanceof Boolean);   //false

Number类型

toFixed()方法会按照指定的小数位返回数值的字符串表示。

toExponential()方法会返回以指数表示法表示的数值的字符串形式。

如果得到某个数值最合适的格式,应该使用toPrecison()方法。可以接收一个参数,表示数值的所有数字的位数。

var num = 99;
alert(num.toPrecision(1));//1e+2
alert(num.toPrecision(2));//99
alert(num.toPrecision(3));//99.0

String类型

  1. 字符方法

charAt()和charCodeAt() 分别是以单字符字符串的形式返回给定位置的那个字符(ECMAScript没有字符类型)和返回给定位置的字符编码。

  1. 字符串操作方法

concat()用于将一或多个字符串拼接起来,返回拼接得到的新字符串。在实践过程中,使用的更多的是+加号操作符。

操作子字符串:slice()、substr()、substring()

方法 第一个参数 第二个参数
slice 开始位置 子字符串最后一个字符后面的位置
substr 开始位置 返回字符的个数
substring 开始位置 子字符串最后一个字符后面的位置

如果是负数

方法 第一个参数 第二个参数
slice 负值与字符串长度相加 赋值与字符串相加
substr 负值与字符串长度相加 转换为0,返回空字符串
substring 负值转换为0,返回整个字符串 第二个负值转换为0,数字小的放第一个参数
  1. 字符串位置方法

indexOf()和lastIndex()方法,第二个参数接收表示从字符串种哪个位置开始搜索。

可以循环调用以上两个方法来找到所有匹配的子字符串。

var stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
var positions = new Array();
var pos = stringValue.indexOf("e");

while(pos > -1){
    positions.push(pos);
    pos = string.Value.indexOf("e", pos + 1);
}

alert(positions);//3,24,32,35,52
  1. trim()方法

创建一个字符串副本,删除前置及后缀的所有空格,然后返回结果。 5. 字符串大小写转换方法 toLowerCase()、toUpperCase(); 6. 字符串的模式匹配方法 match()本质上与调用RegExp的exec()方法相同。只接受一个参数,要么是正则表达式,要么是RegExp对象。

var text = "cat, bat, sat, fat";
var pattern = /.at/;
var matches = text.match(pattern);
alert(matches.index);   //0
alert(matches[0]);      //"cat"
alert(matches.lastIndex);//0

search()返回字符串第一个匹配项的索引,如果没有返回-1。 replace()接收两个参数,第一个参数可以是一个RedExp对象或者一个字符串,第二个参数可以是一个字符串或者一个函数、 第二个参数可以使用一些特殊的字符序列

字符序列 替换文本
$$ $
$& 匹配整个模式的子字符串。与RegExp.lastMatch的值相同
$’ 匹配的字符串之前的子字符串。与RegExp.leftContext的值相同
$` 匹配的字符串之后的子字符串。与RegExp.rightContext的值相同
$n或者$nn 匹配第n个或者第nn个捕获组的子字符串。
var text = "cat, bat, sat, fat";
result = text.replace(/(.at)/g, "word($1)");

alert(result);//word(cat), word(bat), word(sat), word(fat)

replace()方法的第二个参数也可以是一个函数。会向这个函数传递三个参数:模式的匹配项、模式匹配项在字符串中的位置和原始字符串。

function htmlEscape(text){
    return text.replace(/[<>"&]/g,function(match, pos, originalText){
       switch(match){
            case "<":
                return "&lt;";
            case ">":
                return "&gt;";
            case "&":
                return "&amp;";
            case "\"":
                return "&quot;";
       }
    });
}
alert(htmlEscape("<p class=\"greeting\">Hello world!</p>"));
//&lt;p class=&quot;greeting&quot;&gt;Hello world!&lt;/p&gt;

split()可以基于指定的分隔符将一个字符串分割成多个子字符串,第一个参数可以是字符串也可以是RegExp对象,第二个参数可选,指定数组的大小。

var colorText = "red,blue,green,yellow";
var colors1 = colorText.split(",");//["red", "blue", "green", "yellow"]
var colors2 = colorText.split(",",2);//["red", "blue"]
var colors3 = colorText.split(/[^\,]+/);//["", ",", ",", ",", ""]

最后一次调用会出现第一项和最后一项是两个空字符串,是因为正则表达式指定的分隔符出现在了字符串的开头(“red”)和末尾(“yellow”)。 7. localCompare()方法

比较两个字符串,并按照字母表顺序返回。

  • 字符串在字母表种应该排在字符串参数之前,返回一个负数(-1)
  • 等于返回0
  • 字符串在字母表种应该排在字符串参数之后,返回一个负数(1)
  1. fromCharCode()方法

接收一个或多个字符编码然后转换成一个字符串。 9. html方法

单体内置对象

定义:由ECMAScript实现提供的、不依赖于宿主环境的对象,这些对象在ECMAScript程序执行之前就已经存在了。例如Object、Array和String。

Global对象

URI编码方法

encodeURI()和encodeURIComponent()可以对URI(Uniform Resource Identifiers,通用资源标识符)进行编码。

var uri = "http://www.wrox.com/illegal value.htm#start";

console.log(encodeURI(uri));

console.log(encodeURIComponent(uri));
//http://www.wrox.com/illegal%20value.htm#start
//http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start

前者只有空格被替换成了%20,后者替换所有非字母数字字符。

与前面两者对应的是decodeURI()和decodeURIComponent()

eval()方法

它只接受一个参数,即要执行的ECMAScript

Global对象的属性

例如undefined、NaN和Infinity。此外所有原生引用类型的构造函数,像Object和Function,也都是Global对象的属性。

window对象

在全局作用域中声明的所有变量和函数,都成为了window对象的属性。 取得Global对象的方法是使用以下代码:

var global = function(){
    return this;
}();

Math对象

  1. Math对象的属性
属性 说明
Math.e 常量e的值
Math.LN10 10的自然对数
Math.LN2 2的自然对数
Math.LOG2E 以2为底e的对数
Math.LOG10E 以10为底e的对数
Math.PI π的值
Math.SQRT1_2 1/2的平方根
Math.SQRT2 2的平方根
2. min()和max()方法

确定一组数值种的最小值和最大值,可以接收任意多个数值参数。 如果要找到数组中的最大或最小值,可以使用apply()方法。

var values = [1, 2, 3, 4, 5, 6, 7, 8];
var max = Math.max.apply(Math, values);
//max = 8
  1. 舍入方法
  • Math.ceil()向上舍入
  • Math.floor()向下舍入
  • Math.round()标准舍入
  1. random()方法

Math.random()方法返回大于等于0小于1的随机数。

值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值

function selectFrom(lowerValue, upperValue){
    var choices = upperValue - lowerValue + 1;
    return Math.floor(Math.random() * choices + lowerValue);
}
  1. 其他方法
方法 说明 方法 说明
Math.abs(num) num绝对值 Math.asin(x) 反正弦
Math.exp(num) e的num次幂 Math.atan(x) 反正切
Math.log(num) num的自然对数 Math.atan2(y,x) y/x的反正切值
Math.pow(num,power) num的powerci 幂 Math.cos(x) x的余弦值
Math.sqrt(num) num的平方根 Math.sin(x) x的正弦值
Math.acos(x) x的反余弦值 Math.tan(x) x的正切

总结

对象在JavaScript中被称为引用类型的值。

  • 引用类型与传统面向对象程序设计中的类相似,但实现不同。
  • Object是一个基础类型,其他所有类型都从Object继承了基本行为
  • Array类型是一组值的有序列表,同事提供了操作和转换这些值的功能
  • Date类型提供了有关日期和时间的信息,包括当前日期和时间以及相关的计算功能
  • RegExp是ECMAScript支持正则表达式的一个窗口

函数实际上是Function类型的实例,因此函数也是对象。

在所有代码执行之前,作用域种就已经存在两个内置对象:Global和Math,大多数ECMAScript实现中都不能直接访问Global对象的属性。


Profile picture

Written by 何坤舆 EFE