저는 최근 JavaScript 함수를 배우고 있습니다. JavaScript를 잘 배우고 싶다면 함수에 대한 깊은 이해가 있어야 합니다. 나는 학습 과정을 기사로 구성했는데, 첫째는 나 자신의 기능에 대한 이해를 깊게 하고, 둘째는 독자들에게 학습 방법과 우회 방법을 제공하기 위함이었습니다. 내용은 많지만, 작성자의 기능을 요약한 것입니다.
1. 함수 매개변수
1.1: 매개변수란 무엇인가요
1.2: 매개변수 생략
1.3: 기본 매개변수 값
1.4 : 매개변수 전달 방법
1.5: 같은 이름의 매개변수
1.6: 인수 객체
2. 클로저
2.1: 클로저 정의
2.2: 즉시 호출되는 함수 표현식(IIFE, 즉시 호출되는 함수 표현식)
1. 함수 매개변수
1.1: 매개변수란 무엇인가
함수를 정의할 때 , 때로는 추가 데이터를 함수에 전달해야 하는 경우가 있습니다. 이 외부 데이터를 매개변수라고 합니다.
function keith(a){ return a+a; } console.log(keith(3)); //6
위 코드에서는 매개변수 a가 keith 함수에 전달되고 a+a 표현식이 반환됩니다.
1.2: 매개변수 생략
함수 매개변수는 필수가 아니며, JavaScript 사양에서는 호출 시 전달되는 실제 매개변수를 생략하도록 허용합니다.
function keith(a, b, c) { return a; } console.log(keith(1, 2, 3)); //1 console.log(keith(1)); //1 console.log(keith()); // 'undefined'
위 코드에서 keith 함수는 세 개의 매개변수를 정의하지만, 호출 시 매개변수가 아무리 많이 전달되더라도 JavaScript에서는 오류를 보고하지 않습니다. 생략된 매개변수의 기본값은 정의되지 않습니다. 함수 정의와 함수 범위를 이해하는 사람은 함수의 길이 속성이 매개변수 수를 반환한다는 것을 알고 있습니다. 길이 속성은 실제 매개변수의 개수와는 아무런 관련이 없으며, 형식적 매개변수의 개수만 반환한다는 점에 유의해야 합니다.
(실제 매개변수: 호출 시 전달되는 매개변수. 형식 매개변수: 정의 시 전달되는 매개변수.)
그런데 앞의 요소만 생략하고 뒤의 요소를 그대로 유지할 수 있는 방법은 없습니다. 앞의 요소를 생략해야 하는 경우 undefound만 표시됩니다.
function keith(a, b) { return a; } console.log(keith(, 1)); //SyntaxError: expected expression, got ',' console.log(keith(undefined, 2)); //'undefined'
위 코드에서 첫 번째 매개변수가 생략되면 브라우저에서 오류를 보고합니다. 정의되지 않음이 첫 번째 매개변수로 전달되면 오류가 보고되지 않습니다.
1.3: 기본값
JavaScript에서는 함수 매개변수의 기본값이 정의되지 않습니다. 그러나 다른 기본값을 설정하는 것이 유용한 상황이 있습니다. 일반적인 전략은 매개변수 값이 정의되지 않았는지 함수 본문에서 테스트하고, 그렇다면 값을 할당하고, 그렇지 않으면 전달된 실제 매개변수의 값을 반환하는 것입니다.
function keith(a, b) { (typeof b !== 'undefined') ? b = b: b = 1; return a * b; } console.log(keith(15)); //15 console.log(keith(15, 2)) //30
위 코드에서는 판단이 이루어집니다. 호출 시 b 매개변수가 전달되지 않으면 기본값은 1입니다.
ECMAScript 6부터 기본 매개변수가 정의되어 있습니다. 기본 매개변수를 사용하면 함수 본문의 검사가 더 이상 필요하지 않습니다.
function keith(a, b = 1) { return a * b; } console.log(keith(15)); //15 console.log(keith(15, 2)) //30
1.4: 매개변수 전달 방법
함수 매개변수를 전달하는 방법에는 두 가지가 있는데, 하나는 값으로 전달하는 것이고 다른 하나는 주소로 전달하는 것입니다.
함수 매개변수가 원시 데이터 유형(문자열, 숫자 값, 부울 값)인 경우 매개변수 전달 방법은 값을 기준으로 합니다. 즉, 함수 본문 내에서 매개변수 값을 수정해도 함수 외부에는 영향을 미치지 않습니다.
var a = 1; function keith(num) { num = 5; } keith(a); console.log(a); //1
위 코드에서 전역 변수 a는 원시형 값이고, keith 함수를 전달하는 방식은 값을 기준으로 합니다. 따라서 함수 내에서 a의 값은 원래 값의 복사본이므로 어떻게 수정하더라도 원래 값에는 영향을 주지 않습니다.
단, 함수 매개변수가 복합형 값(배열, 객체, 기타 함수)인 경우 전달 방식은 참조로 전달됩니다. 즉, 함수에 전달되는 것은 원래 값의 주소이므로 함수 내부의 매개변수를 수정하면 원래 값에 영향을 미치게 됩니다.
var arr = [2, 5]; function keith(Arr) { Arr[0] = 3; } keith(arr); console.log(arr[0]); //3
위 코드에서 keith 함수에 전달되는 것은 매개변수 객체 arr의 주소입니다. 따라서 함수 내에서 arr의 첫 번째 값을 수정하면 원래 값에 영향을 미칩니다.
함수 내부에서 수정된 내용이 매개변수 개체의 특정 속성이 아니라 매개변수 전체를 대체하는 경우 원래 값에는 영향을 미치지 않는다는 점에 유의하세요.
var arr = [2, 3, 5]; function keith(Arr) { Arr = [1, 2, 3]; } keith(arr); console.log(arr); // [2,3,5]
위 코드에서 keith 함수 내에서 매개변수 개체 arr은 다른 값으로 완전히 대체됩니다. 이때 원래 값은 영향을 받지 않습니다. 이는 형식 매개변수(Arr)와 실제 매개변수 arr 사이에 할당 관계가 있기 때문입니다.
1.5: 동일한 이름의 매개변수
동일한 이름의 매개변수가 있는 경우 마지막 매개변수의 값이 제공되지 않으면 마지막에 나타나는 값을 가져옵니다. 정의되지 않은 상태가 됩니다.
function keith(a, a) { return a; } console.log(keith(1, 3)); //3 console.log(keith(1)); //undefined
같은 이름의 매개변수의 첫 번째 매개변수에 접근하려면 인수 객체를 사용하세요.
function keith(a, a) { return arguments[0]; } console.log(keith(2)); //2
1.6 인수 객체
JavaScript의 모든 함수는 특수 변수 인수에 액세스할 수 있습니다. 이 변수는 이 함수에 전달된 모든 매개변수 목록을 유지합니다.
인수 개체에는 함수가 실행될 때 모든 매개 변수가 포함됩니다. 인수[0]은 첫 번째 매개 변수이고 인수[1]은 두 번째 매개 변수입니다. 이 객체는 함수 본문 내에서만 사용할 수 있습니다.
인수 개체의 길이 속성에 액세스하여 함수 호출에 포함되는 매개변수 수를 확인할 수 있습니다.
function keith(a, b, c) { console.log(arguments[0]); //1 console.log(arguments[2]); //3 console.log(arguments.length); //4 } keith(1, 2, 3, 4);
인수 객체와 배열의 관계
arguments 对象不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个类数组对象。因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice。但是可以使用数组中的length属性。
通常使用如下方法把arguments对象转换为数组。
var arr = Array.prototype.slice.call(arguments);
2.闭包
2.1:闭包定义
要理解闭包,需要先理解全局作用域和局部作用域的区别。函数内部可以访问全局作用域下定义的全局变量,而函数外部却无法访问到函数内部定义(局部作用域)的局部变量。
var a = 1; function keith() { return a; var b = 2; } console.log(keith()); //1 console.log(b); //ReferenceError: b is not defined
上面代码中,全局变量a可以在函数keith内部访问。可是局部变量b却无法在函数外部访问。
如果需要得到函数内部的局部变量,只有通过在函数的内部,再定义一个函数。
function keith(){ var a=1; function rascal(){ return a; } return rascal; } var result=keith(); console.log(result()); //1 function keith(){ var a=1; return function(){ return a; }; } var result=keith(); console.log(result()) //1
上面代码中,两种写法相同,唯一的区别是内部函数是否是匿名函数。函数rascal就在函数keith内部,这时keith内部的所有局部变量,对rascal都是可见的。但是反过来就不行,rascal内部的局部变量,对keith就是不可见的。这就是JavaScript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。函数keith的返回值就是函数rascal,由于rascal可以读取keith的内部变量,所以就可以在外部获得keith的内部变量了。
闭包就是函数rascal,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如rascal记住了它诞生的环境keith,所以从rascal可以得到keith的内部变量。
闭包可以使得它诞生环境一直存在。看下面一个例子,闭包使得内部变量记住上一次调用时的运算结果。
function keith(num) { return function() { return num++; }; } var result = keith(2); console.log(result()) //2 console.log(result()) //3 console.log(result()) //4
上面代码中,参数num其实就相当于函数keith内部定义的局部变量。通过闭包,num的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包result使得函数keith的内部环境,一直存在。
通过以上的例子,总结一下闭包的特点:
1:在一个函数内部定义另外一个函数,并且返回内部函数或者立即执行内部函数。
2:内部函数可以读取外部函数定义的局部变量
3:让局部变量始终保存在内存中。也就是说,闭包可以使得它诞生环境一直存在。
闭包的另一个用处,是封装对象的私有属性和私有方法。
function Keith(name) { var age; function setAge(n) { age = n; } function getAge() { return age; } return { name: name, setAge: setAge, getAge: getAge }; } var person = Keith('keith'); person.setAge(21); console.log(person.name); // 'keith' console.log(person.getAge()); //21
2.2:立即调用的函数表达式(IIFE)
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
循环中的闭包
一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号
for(var i=0;i<10;i++){ setTimeout(function(){ console.log(i); //10 }, 1000) }
上面代码中,不会符合我们的预期,输出数字0-9。而是会输出数字10十次。
当匿名函数被调用的时候,匿名函数保持着对全局变量 i 的引用,也就是说会记住i循环时执行的结果。此时for循环结束,i 的值被修改成了10。
为了得到想要的效果,避免引用错误,我们应该使用IIFE来在每次循环中创建全局变量 i 的拷贝。
for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); //1,2,3,....,10 }, 1000); })(i); }
外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。