(60)Wangdao.com第十天_JavaScript 函数_作用域_闭包_IIFE_回调函数_eval
函数
实现特定功能的 n 条语句封装体。
1. 创建一个函数对象
var myFunc = new Function(); // typeof myFunc 将会打印 function
- 将要封装的代码以字符串的方式传递给构造函数
-
var aFunc = new Function("console.log('Hello!');"); console.log(aFunc); // 打印: " function anonymous(){ console.log("Hello!"); } "
-
- 使用函数声明来创建一个函数
-
function myFunc(){ console.log("Hello myFunc !"); }
// 会 函数声明 提升
-
- 使用函数表达式来创建一个函数
-
var myFunc = function(){ console.log("Hello myFunc ! "); };
// 会 变量声明 提升 -
var myAdd = function(a,b){ // 传递多个参数使用,隔开 console.log(a+b); };
注意:函数解析器不会检查实参的类型; 多余的参数将不会被使用; 缺少的参数定义为 undefined
-
2. 函数的返回值 外界需要函数处理的值
使用 return 关键字返回指定结果,并结束函数。
不写 return 默认为 return ; 此时的函数返回值为 undefined
3. 函数的参数 当无法确定 n 个变量的值时, 使用 n 个参数 传递____形参
- 函数的
length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数length
属性就是定义时的参数个数。不管调用时输入了多少个参数,length
属性始终等于 形参个数function f(a, b){} f.length // 2
- 同名参数
- 如果有同名的参数,则取最后出现的那个值
function f(a, a) { console.log(a); } f(1, 2) // 2 f(1) // undefined // 这时,如果要获得第一个a的值,可以使用arguments对象。 function f(a, a) { console.log(arguments[0]); } f(1) // 1
- 如果有同名的参数,则取最后出现的那个值
只有函数调用时,才能动态确定 this 的指向
.call(obj, 12, 13);
.apply(obj, [12, 13]);
.bind(obj, 12, 13); 重构建函数
不会立即执行函数,而是会创建一个新函数,新函数的 this 指向 obj,参数也被传递
也不会修改原函数的 this
arguments 实参列表
对象 只有数组的 length 属性,能够通过 index 读写数据- 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。
- 包含了函数运行时的所有参数,
arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。var f = function (one) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f(1, 2, 3) // 1 // 2 // 3
- 正常模式下,
arguments
对象可以在运行时修改。var f = function(a, b) { arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 5
- 严格模式下,
arguments
对象是一个只读对象,修改它是无效的,但不会报错。var f = function(a, b) { 'use strict'; // 开启严格模式 arguments[0] = 3; // 无效 arguments[1] = 2; // 无效 return a + b; } f(1, 1) // 2
- 正常模式下,
- 通过
arguments
对象的length
属性,可以判断函数调用时到底带几个参数。 - 虽然
arguments
很像数组,但它是一个对象。数组专有的方法(比如slice
和forEach
),不能在arguments
对象上直接使用。- 如果要让
arguments
对象使用数组方法,真正的解决方法是将arguments
转为真正的数组。下面是两种常用的转换方法://slice方法 var args = Array.prototype.slice.call(arguments);
//逐一填入新数组。 var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); }
- 如果要让
arguments
.callee
属性,返回它所对应的原函数-
var f = function () { console.log(arguments.callee === f); } f() // true // 可以通过arguments.callee,达到调用函数自身的目的。 // 这个属性在严格模式里面是禁用的,因此不建议使用。
-
- 实参可以是任意数据类型。
- 函数参数不是必需的,Javascript 允许省略参数
- 运行时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值就变为
undefined
- 但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入
undefined
function f(a, b) { return a; } f( , 1) // 报错 SyntaxError: Unexpected token ,(…) f(undefined, 1) // undefined
- 运行时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值就变为
- 函数被称为 "第一等公民"
- 将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。
- 当数据有规律,有必要的话可以存到一个对象里,传入函数。
-
var sun = { name:"孙悟空", gender:"男", age:600 }; function sayHello(obj){ document.write("大家好,我是"+obj.name+", "+obj.gender+", "+obj.age+"岁了"); } sayHello(sun);
-
- 实参还可以是函数
-
function haha(someFunc){ someFunc(); } haha(sayHello);
-
- 返回值可以对象
- 返回值也可以是函数
- break; 用于终止函数
- continue; 用于终止本次循环,直接开始下一次循环
- return; 用于设置返回值,也会马上结束函数
4. 函数的属性和方法
.name 属性,返回函数的名字
-
function f1() {} f1.name // "f1" var f2 = function () {}; f2.name // "f2" var f3 = function myName() {}; f3.name // 'myName'
-
.toString()
方法返回一个字符串,内容是函数的源码-
function f() { a(); b(); c(); // 这是注释 } f.toString() // function f() { // a(); // b(); // c(); // 这是注释 // }
-
5. 作用域(scope)
变量存在的范围。
不同作用域的同名变量不会冲突
- 在 ES5 的规范中,Javascript 只有两种作用域:
- 必会面试题
-
function Foo() { getName = function () { alert (1); }; return this; } Foo.getName = function () { alert (2);}; Foo.prototype.getName = function () { alert (3);}; var getName = function () { alert (4);}; function getName() { alert (5);} //请写出以下输出结果: Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName();
- 分析:
-
var getName; window.getName = function () { alert (4);}; function Foo() { // window.Foo getName = function () { alert (1); }; // 覆盖 window.getName return this; }; window.Foo.prototype.getName = function () { alert (3);}; window.Foo.getName = function () { alert (2);}; //请写出以下输出结果: Foo.getName(); // window.Foo.getName() 2 getName(); // window.getName() 4 // this.getName() window.getName() 1 注意是在什么时候被覆盖的 Foo().getName();
getName(); // window.getName() 1 new Foo.getName(); // window.Foo.getName() 2 // 注意 new 返回的是对象 直接调用返回看return // f.getName() window.f.getName() window.Foo.prototype.getName 3 new Foo().getName();
new new Foo().getName(); // new window.f.getName() 3 - ES6 又新增了块级作用域
6. 闭包 closure
① 嵌套: 作为函数里面的 函数 fn2
② fn2 引用了外部函数的局部变量
③ 外部函数 被调用执行时产生闭包,在 new 的时候也相当于执行了____实质是在 第一个满足以上条件 的函数被解析到是,产生闭包
此时就产生了一个闭包____ fn2.Scopes[0] = fn2.closure————包含了被引用局部变量的一个"对象",存在于嵌套的内部函数中,可以使用调试工具查看到。
其中 [[Scopes]] 是由底层 c/c++ 实现
- 一个外层函数,嵌套多个函数,这几个内部函数 共用一个 闭包,该闭包不会在外层函数执行完后销毁,也就意味着延长了变量的存活时间
- 闭包内的变量,无法直接操作,只能通过内层函数间接操作闭包内的变量
- 闭包存在于 内部函数 fn2 中, 只要 fn2 存在,则闭包存在____当再也没有 变量 指向这个 fn2 ,则 fn2 不存在,闭包也就不存在了
缺点:
由于闭包的存在,未释放,导致占用内存时间变长。易造成内存泄漏
怕的就是出现 非常多个闭包
解决:
能不用闭包就不用闭包
及时释放____让 指向含闭包函数的 变量,指向 null
- 作用有
- 使得可以 在函数外部 间接操作 函数内部的变量。
- 让这些变量始终保持在内存中, 不会在函数调用结束后,被垃圾回收机制回收。
- 封装对象的私有属性和私有方法。
-
function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name: name, getAge: getAge, setAge: setAge }; } var p1 = new Person('张三'); p1.setAge(25); // 操作的是 闭包中的 _age p1.getAge() // 访问的是 闭包中的 _age
// 上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。注意
-
外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。
-
因此不能滥用闭包,否则会造成网页的性能问题。
-
-
- 是Javascript 语言的一个难点,也是它的特色。
- 闭包最大的特点,就是它可以 “记住” 诞生的环境,比如
fn2
记住了它诞生的环境fn1
的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 - 很多高级应用都要依靠闭包实现。
- 理解闭包,首先必须理解变量作用域。
- "作用域链"(scope chain)
- JavaScript 语言特有的结构
- 子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
- 正常情况下,得到函数内的局部变量是办不到的
- 只有通过变通方法才能实现
- 那就是在函数的内部,再定义一个函数。
- 既然子函数可以读取父函数的局部变量,那么只要把子函数作为返回值,我们不就可以在父函数外部读取它的内部变量了吗!
function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999
// 函数f1
的返回值就是函数f2
,由于f2
可以读取f1
的内部变量,所以就可以在外部获得f1
的内部变量了 - 闭包就是返回的 子函数
f2
,即能够读取其他函数内部变量的函数。
- 请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
-
function createIncrementor(start) { return function () { return start++; }; } var inc = createIncrementor(5); // 执行了 外层函数,然后销毁了 函数上下文,即 所有外层函数的变量都销毁了 inc(); // 5 inc(); // 6 inc(); // 7
-
7. 立即调用的函数表达式(IIFE)
自调用匿名函数
____封装时, 隐藏函数内部具体实现, 防止外部修改函数内的代码
____防止污染外部命名空间
-
- 以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,避免出现错误。
这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。
-
- 目的有两个:
- 一是不必为函数命名,避免了污染全局变量;
- 二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
// 写法一 var tmp = newData; processData(tmp); storeData(tmp); // 写法二 更好,因为完全避免了污染全局变量。 (function(w){ // var tmp = newData; processData(tmp); storeData(tmp); }(window)); // 方便对代码进行压缩, 变量 window 改成 w, 减小代码体积
- 目的有两个:
- 圆括号
()
是一种运算符,跟在函数名之后,表示调用该函数。 比如,isNaN()
就表示调用 isNaN - 当
function
出现在行首,为了不让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
8. 回调函数
自定义的,不被自己调用的,达到某种条件后被某对象调用了。
异步加载通知机制:
所有异步代码,不能立即执行
只有等主进程执行完所有代码后,才会执行 异步代码
-
setTimeout(function(){ console.log("异步代码"); }, 0); console.log("主进程代码");
// 打印:
// 主进程代码
// 异步代码
9. eval
命令
接受一个字符串作为参数,并将这个字符串当作语句执行。
eval('var a = 1;'); a // 1
- 如果参数字符串无法当作语句运行,那么就会报错。
// Uncaught SyntaxError: Invalid or unexpected token
- 如果
eval
的参数不是字符串,那么会原样返回。 eval
没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。- JavaScript 规定,如果使用严格模式,
eval
内部声明的变量,不会影响到外部作用域。- 不过,即使在严格模式下,
eval
依然可以读写当前作用域的变量。(function f() { 'use strict'; var foo = 1; eval('foo = 2'); console.log(foo); // 2 })()
所以一般不推荐使用。通常情况下,
eval
最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse
方法。
- 不过,即使在严格模式下,
eval
的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨eval()
这一种形式是直接调用。eval.call(null, '...'); window.eval('...'); (1, eval)('...'); (eval, eval)('...'); // 上面这些形式都是eval的别名调用,作用域都是全局作用域。
内存溢出
需要的内存,大于现有内存
内存泄漏
资源占有一片空间,而未曾释放,导致这片内存无法被使用
及时 obj = null; 来释放内存
- 意外的全局变量
在函数中, 未使用 var 关键字定义变量, 导致直接声明了一个全局变量
- 没有及时清理的 定时器 或者 回调函数
- 闭包
10. 属性描述符