image.png

函数的定义与调用

函数的定义

函数的定义大体上可以分为3种,分别是函数声明函数表达式Function构造函数

1. 函数声明

函数声明是直接使用function关键字接一个函数名,示例如下:

  1. // 函数声明式
  2. function sum(num1, num2) {
  3. return num1 + num2;
  4. }

2. 函数表达式

函数表达式的形式类似于普通变量的初始化,只不过这个变量初始化的值是一个函数,示例如下:

  1. // 该函数表达式没有名称,属于匿名函数表达式。匿名函数的this是全局对象window
  2. var sum = function (num1, num2) {
  3. console.log(this) // window
  4. return num1 + num2;
  5. };
  6. // 具有函数名的函数表达式
  7. var sum = function foo(num1, num2) {
  8. return num1 + num2;
  9. };
  10. console.log(foo(1,2)) // ReferenceError: foo is not defined
  11. // 其中foo是函数名称,它实际是函数内部的一个局部变量,在函数外部是无法直接调用的

应用场景
(1)函数递归

  1. // 函数声明
  2. function bonacci(num) {
  3. if (num === 1 || num === 2) {
  4. return 1
  5. };
  6. return bonacci(num - 2) + bonacci(num - 1)
  7. }
  8. // 函数表达式
  9. var bonacci = function (num) {
  10. if (num === 1 || num === 2) {
  11. return 1
  12. };
  13. return bonacci(num - 2) + bonacci(num - 1)
  14. }

(2)代码模块化
在ES6以前,JavaScript中是没有块级作用域的,但是我们可以通过函数表达式来间接地实现模块化,将特定的模块代码封装在一个函数中,只对外暴露接口,使用者也不用关心具体细节,这样做可以很好地避免全局环境的污染。

  1. // 创建一个立即执行的匿名函数表达式,返回的是一个对象
  2. // 使用者只需要调用getName()函数和setName()函数,而不用关心person私有的_name属性
  3. var person = (function () {
  4. var _name = ""
  5. return {
  6. getName: function () {
  7. return _name
  8. },
  9. setName: function (newName) {
  10. _name = newName
  11. }
  12. }
  13. }())
  14. person.setName('kingx')
  15. person.getName() // 'kingx'

3. Function()构造函数

使用new操作符,调用Function()构造函数,传入对应的参数,也可以定义一个函数,示例如下:

  1. // 其中的参数,除了最后一个参数是执行的函数体,其他参数都是函数的形参。
  2. var add = new Function("a", "b", "return a + b");

相比于函数声明和函数表达式这两种方式,Function()构造函数的使用比较少,主要有以下两个原因:

  1. Function()构造函数每次执行时,都会解析函数主体,并创建一个新的函数对象,所以当在一个循环或者频繁执行的函数中调用Function()构造函数时,效率是非常低的。
  2. 所以在一个函数A内部调用Function()构造函数时,其中的函数体并不能访问到函数A中的局部变量,而只能访问到全局变量。
    1. var y = 'global'; // 全局环境定义的y值
    2. function constructFunction() {
    3. var y = 'local'; // 局部环境定义的y值
    4. return new Function('return y'); // 无法获取局部环境定义的值
    5. }
    6. console.log(constructFunction()()); // 输出'global'

    4. 函数声明与函数表达式的区别

    (1)函数名称。函数声明必须设置函数名称,这个函数名称相当于一个变量,以后函数的调用也会通过这个变量进行。函数表达式是可选的,可以定义一个匿名函数表达式,也可以定义一个具名函数表达式。
    (2)函数提升。对于函数声明,存在函数提升,所以即使函数的调用在函数的声明之前,仍然可以正常执行。对于函数表达式,不存在函数提升,所以在函数定义之前,不能对其进行调用,否则会抛出异常。
    1. console.log(add(1, 2)); // "3"
    2. console.log(sub(5, 3)); // Uncaught TypeError: sub is not a function
    3. // 函数声明
    4. function add(a1, a2) {
    5. return a1 + a2;
    6. }
    7. // 函数表达式
    8. var sub = function (a1, a2) {
    9. return a1 - a2;
    10. };

    函数的调用

    函数的调用存在5种模式,分别是函数调用模式方法调用模式构造器调用模式call()apply()函数调用模式匿名函数调用模式

    1. 函数调用模式

    函数调用模式是通过函数声明或者函数表达式的方式定义函数,然后直接通过函数名调用的模式。
    1. // 函数声明
    2. function add(a1, a2) {
    3. return a1 + a2;
    4. }
    5. // 函数表达式
    6. var sub = function (a1, a2) {
    7. return a1 - a2;
    8. };
    9. add(1, 3);
    10. sub(4, 1);

    2. 方法调用模式

    方法调用模式会优先定义一个对象obj,然后在对象内部定义值为函数的属性property,通过对象obj.property()来进行函数的调用。匿名函数的this是全局对象window,所以会有回调地狱 ```javascript var obj = { name: ‘kingx’, // 定义getName属性,值为一个函数 getName: function () { return this.name; } }; obj.getName(); // 通过对象进行调用 obj‘getName’;

// 如果在某个方法中返回的是函数对象本身this,那么可以利用链式调用原理进行连续的函数调用。 var obj2 = { name: ‘kingx’, getName: function () { console.log(this.name) }, setName: function (name) { this.name = name return this // 在函数内部返回函数对象本身 }, test: function () { // 用_this保存obj中的this var _this = this return function () { // 匿名函数 console.log(this) // window return _this.name } } } obj2.setName(‘kingx2’).getName(); // 链式函数调用

  1. <a name="Dd9Vt"></a>
  2. ### 3. 构造器调用模式
  3. 构造器调用模式会定义一个函数,在函数中定义实例属性,在原型上定义函数,然后通过new操作符生成函数的实例,再通过实例调用原型上定义的函数。
  4. ```javascript
  5. // 定义函数对象
  6. function Person(name) {
  7. this.name = name;
  8. }
  9. // 原型上定义函数
  10. Person.prototype.getName = function () {
  11. return this.name;
  12. };
  13. // 通过new操作符生成实例
  14. var p = new Person('kingx');
  15. // 通过实例进行函数的调用
  16. p.getName();

4. call()、apply()、bind()函数调用模式

call()、apply()、bind()的比较:
相同之处:通过call()函数、apply()函数、bind()函数可以改变函数执行的主体,改变this的指向,使得某些不具有特定函数的对象可以直接调用该特定函数。
不同之处

  1. 关于函数立即执行。call()函数与apply()函数在执行后会立即调用前面的函数,而bind()函数不会立即调用,它会返回一个新的函数,可以在任何时候进行调用。要想bind函数立即执行也可以直接**function.bind(obj, arg1, arg2, ···)()**
  2. 关于参数传递。第一个参数都表示将要改变的函数执行主体,即this的指向;第二个参数call()函数与bind()一个一个列出来的参数,而apply()函数是一个数组(类似于展开运算符,将数组转化为参数),并且如果apply()函数第二个参数不是一个有效的数组或者类数组arguments对象,则会抛出一个TypeError异常。 ```javascript // 定义一个函数 function sum(num1, num2) { return num1 + num2; } // 定义一个对象 var person = {};

sum.call(person, 1, 2); sum.apply(person, [1, 2]); let newFunc = sum.bind(person, 1, 2) newFunc() // 3 // 或者 sum.bind(person, 1, 2)() // 3

  1. **应用**:
  2. 1. 求数组中的最大项和最小项
  3. ```javascript
  4. var arr = [3, 5, 7, 2, 9, 11];
  5. // 求数组中的最大值
  6. console.log(Math.max.apply(null, arr)); // 11
  7. // 求数组中的最小值
  8. console.log(Math.min.apply(null, arr)); // 2
  1. 类数组对象转换为数组对象 ```javascript // 任意个数字的求和 function sum() { // arguments是一个类数组对象,自身不能直接调用数组的方法,将传递的参数转换为数组 var arr = Array.prototype.slice.call(arguments); // 调用数组的reduce()函数 return arr.reduce(function (pre, cur) { return pre + cur; }, 0) }

sum(1, 2); // 3 sum(1, 2, 3); // 6 sum(1, 2, 3, 4); // 10

  1. 3. 执行匿名函数
  2. 场景:有一个数组,数组中的每个元素是一个对象,对象是由不同的属性构成,现在我们想要调用一个函数,输出每个对象的各个属性值。
  3. ```javascript
  4. var animals = [
  5. { species: 'Lion', name: 'King' },
  6. { species: 'Whale', name: 'Fail' }
  7. ]
  8. for (var i = 0; i < animals.length; i++) {
  9. (function (i) {
  10. this.print = function () {
  11. console.log('#' + i + ' ' + this.species + ': ' + this.name)
  12. }
  13. this.print()
  14. }).call(animals[i], i)
  15. }
  1. 用于继承 ```javascript // 构造继承用到call()函数

// 父类 function Animal(age) { this.age = age this.sleep = function () { return this.name + ‘正在睡觉!’ } } // 子类 function Cat(name, age) { // 使用call()函数实现继承 Animal.call(this, age) this.name = name || ‘tom’ }

var cat = new Cat(‘tony’, 11) console.log(cat.sleep()) // tony正在睡觉! console.log(cat.age) // 11

  1. 5. bind()函数配合setTimeout(不能用callapply,会立即执行)
  2. ```javascript
  3. function LateBloomer() {
  4. this.petalCount = Math.ceil(Math.random() * 12) + 1
  5. }
  6. LateBloomer.prototype.bloom = function () {
  7. // setTimeout第一个参数接受的函数是匿名函数,this指向window,所以用bind改变this指向
  8. setTimeout(this.declare.bind(this), 1000)
  9. // 或者 var that = this
  10. // setTimeout(function () {
  11. // that.declare()
  12. // }, 1000)
  13. // 但是setTimeout(this.declare, 1000) 则会undefined
  14. }
  15. LateBloomer.prototype.declare = function () {
  16. console.log('petalCount:' + this.petalCount)
  17. }
  18. var flower = new LateBloomer()
  19. flower.bloom() // 1秒后,调用declare()函数

5. 匿名函数调用模式

匿名函数,顾名思义就是没有函数名称的函数。

  1. // 一种是通过匿名函数表达式
  2. var sum = function(num1, num2){
  3. return num1 + num2;
  4. };
  5. sum(1, 2);
  6. // 函数表达式后跟小括号()表示的是函数立即执行
  7. var sum = function (num1, num2) {
  8. return num1 + num2;
  9. }(1, 2);
  10. console.log(sum); // 3
  11. // 另一种是使用小括号()将匿名函数括起来
  12. (function (num1, num2) {
  13. return num1 + num2;
  14. })(1, 2); // 3

变量提升与函数提升

变量提升

只提升变量名,不提升变量值
会产生提升的变量必须是通过var关键字定义的,而不通过var关键字定义的全局变量是不会产生变量提升的。

  1. (function () {
  2. console.log(v) // Uncaught ReferenceError: v is not defined
  3. v = 'Hello JavaScript'
  4. })();

并且变量提升不受if语句影响(即if语句为false,也会进去执行解析变量声明)

  1. (function foo() {
  2. if (a) {
  3. var a = 10
  4. }
  5. console.log(a) // undefined
  6. })()

函数提升

函数声明会进行函数提示提升,会将整个函数体一起进行提升,包括里面的执行逻辑。函数表达式,是不会进行函数提升的

  1. show() // 你好
  2. var show
  3. // 函数声明,会被提升
  4. function show() {
  5. console.log('你好')
  6. }
  7. // 函数表达式,不会被提升
  8. show = function () {
  9. console.log('hello')
  10. }

并且在函数内,函数声明提升不受return语句的影响(即在return语句之后声明也会执行解析,进行函数提示)

  1. function foo() {
  2. function bar() {
  3. return 3
  4. }
  5. console.log(test()) // 函数声明会提前,不受return语句影响
  6. return bar()
  7. function bar() { // 所以后一个bar()覆盖前一个bar()
  8. return 8
  9. }
  10. function test() {
  11. return '函数声明会提前'
  12. }
  13. }
  14. console.log(foo()) // 8

变量提升和函数提升优先级:变量提升的优先级要比函数提升的优先级高

  1. fn()
  2. function fn() {
  3. console.log(typeof foo) // function
  4. // 变量提升
  5. var foo = 'variable'
  6. // 函数提升
  7. function foo() {
  8. return 'function'
  9. }
  10. console.log(typeof foo) // string
  11. }
  12. // 改写相当于:
  13. fn()
  14. function fn() {
  15. // 变量提升至函数顶部
  16. var foo
  17. // 函数提升,但是优先级低,出现在变量声明后面,则foo是一个函数
  18. function foo() {
  19. return 'function'
  20. }
  21. console.log(typeof foo) // function
  22. foo = 'variable' // 变量赋值
  23. console.log(typeof foo) // string
  24. }

自执行函数

自执行函数即函数定义和函数调用的行为先后连续产生

  1. function (x) {
  2. alert(x)
  3. } (5) // 抛出异常,Uncaught SyntaxError: Unexpected token (
  4. var aa = function (x) { // 匿名函数表达式
  5. console.log(x)
  6. }(1) // 1
  7. true && function (x) {
  8. console.log(x)
  9. }(2) // 2
  10. 0, function (x) {
  11. console.log(x)
  12. }(3) // 3
  13. !function (x) {
  14. console.log(x)
  15. }(4) // 4
  16. ~function (x) {
  17. console.log(x)
  18. }(5); // 5
  19. -function (x) {
  20. console.log(x)
  21. }(6); // 6
  22. +function (x) {
  23. console.log(x)
  24. }(7) // 7
  25. new function () {
  26. console.log(8) // 8
  27. }
  28. new function (x) {
  29. console.log(x)
  30. }(9) // 9

函数参数

实参形参

当实参是基本数据类型的值时:实际是将实参的值复制一份传递给形参,在函数运行结束时形参被释放,而实参中的值不会变化。
当实参是引用类型的值时:实际是将实参的内存地址传递给形参,即实参和形参都指向相同的内存地址,此时形参可以修改实参的值,但是不能修改实参的内存地址

  1. var arg = { name: 'kingx' } // 定义一个实参arg为一个对象
  2. function fn(param) {
  3. param.name = 'kingx2' // 修改形参param的属性值,此时形参param与实参arg指向的是同一个内存地址
  4. param = {} // 将形参param指向了一个新的内存地址,但是这并不会影响实参arg的值
  5. }
  6. fn(arg)
  7. console.log(arg) // {name: "kingx2"}

由于JavaScript是一门弱类型的语言,函数参数在遵循上述规则的基础上,还具有以下几个特性:

  • 函数可以不用定义形参,可以在函数体中通过arguments对象获取传递的实参并进行处理。
  • 在函数定义了形参的情况下,传递的实参与形参的个数并不需要相同,实参与形参会从前到后匹配,未匹配到的形参被当作undefined处理
  • 实参并不需要与形参的数据类型一致,因为形参的数据类型只有在执行期间才能确定,并且还存在隐式数据类型的转换。

由于这些特点的存在,函数参数的处理非常灵活,其中最关键的一点是,JavaScript为函数增加了一个内置的arguments对象。

arguments对象的性质

介绍
**arguments**对象是所有函数都具有的一个内置局部变量,表示的是函数实际接收的参数,是一个类数组结构(除了具有length属性外,不具有数组的一些常用方法)。
性质
1. 函数外部无法访问
**arguments**对象只能在函数内部使用,无法在函数外部访问到arguments对象。
同时**arguments**对象存在于函数级作用域中,一个函数无法直接获取另一个函数的arguments对象。

  1. console.log(typeof arguments) // undefined
  2. function foo() {
  3. console.log(arguments.length) // 3
  4. function foo2() {
  5. console.log(arguments.length) // 0
  6. }
  7. foo2()
  8. }
  9. foo(1, 2, 3)

2. 可通过索引访问
**arguments**对象是一个类数组结构,可以通过索引访问,每一项表示对应传递的实参值,如果该项索引值不存在,则会返回“undefined”。

  1. function sum(num1, num2) {
  2. console.log(arguments[0]) // 3
  3. console.log(arguments[1]) // 4
  4. console.log(arguments[2]) // undefined
  5. }
  6. sum(3, 4)

3. 由实参决定
**arguments**对象的值由实参决定,而不是由定义的形参决定,形参与arguments对象占用独立的内存空间。
arguments对象与形参之间的关系

  • arguments对象的length属性在函数调用的时候就已经确定,不会随着函数的处理而改变。
  • 指定的形参在传递实参的情况下,arguments对象与形参值相同,并且可以相互改变
  • 指定的形参在未传递实参的情况下,arguments对象与形参值不能相互改变,并且arguments对象对应索引值返回“undefined”。 ```javascript foo(1, 2)

function foo(a, b, c) { console.log(arguments.length) // 2

// 在形参a、b都传递了实参的情况下,对应的arguments[0]与arguments[1]与a和b相互影响 arguments[0] = 11 console.log(a) // 11 console.log(arguments[1]) // 2 b = 12 console.log(arguments[1]) // 12

// 形参c未传递实参,对arguments[2]值的设置不会影响到c值,对c值的设置也不会影响到arguments[2] arguments[2] = 3 console.log(c) // undefined

c = 13 // 将形参赋值 console.log(arguments[2]) // 3

console.log(arguments.length) // 2 arguments对象的length属性是由实际传递的参数个数决定的 }

  1. **4. 特殊的arguments.callee属性**<br />arguments对象有一个很特殊的属性callee,表示的是当前正在执行的函数,在比较时是严格相等的。
  2. ```javascript
  3. foo()
  4. function foo() {
  5. console.log(arguments.callee === foo) // true
  6. }

通过arguments.callee属性获取到函数对象后,可以直接传递参数重新进行函数的调用,这个属性在匿名的递归函数中非常有用。

  1. function create() {
  2. return function (n) {
  3. if (n <= 1)
  4. return 1
  5. return n * arguments.callee(n - 1)
  6. }
  7. }
  8. var result = create()(5) // returns 120 (5 * 4 * 3 * 2 * 1)

在上面的代码中,create()函数返回一个匿名函数,在匿名函数内部需要对自身进行调用,因为匿名函数没有函数名称,所以只能通过arguments.callee属性获取函数自身,同时传递参数进行函数调用。
尽管arguments.callee属性可以用于获取函数本身去做递归调用,但是我们并不推荐广泛使用arguments.callee属性,其中有一个主要原因是使用arguments.callee属性后会改变函数内部的this值。

  1. var sillyFunction = function (recursed) {
  2. if (!recursed) {
  3. console.log(this) // Window {}
  4. return arguments.callee(true)
  5. }
  6. console.log(this) // Arguments {}
  7. }
  8. sillyFunction()

如果需要在函数内部进行递归调用,推荐使用函数声明或者使用函数表达式,给函数一个明确的函数名。

arguments对象的应用

1. 实参的个数判断
定义一个函数,明确要求在调用时只能传递3个参数,如果传递的参数个数不等于3,则直接抛出异常。

  1. function f(x, y, z) {
  2. // 检查传递的参数个数是否正确
  3. if (arguments.length !== 3) {
  4. throw new Error("期望传递的参数个数为3,实际传递个数为" + arguments.length)
  5. }
  6. // ...do something
  7. }
  8. f(1, 2) // Uncaught Error: 期望传递的参数个数为3,实际传递个数为2

2. 任意个数的参数处理
定义一个函数,该函数只会特定处理传递的前几个参数,对于后面的参数不论传递多少个都会统一处理。例如,定义一个函数,需要将多个字符串使用分隔符相连,并返回一个结果字符串。此时第一个参数表示的是分隔符,而后面的所有参数表示待相连的字符串,我们并不关心后面待连接的字符串有多少个,通过arguments对象统一处理即可。

  1. function joinStr(seperator) {
  2. // arguments对象是一个类数组结构,可以通过call()函数间接调用slice()函数,得到一个数组
  3. var strArr = Array.prototype.slice.call(arguments, 1)
  4. // strArr数组直接调用join()函数
  5. return strArr.join(seperator)
  6. }
  7. joinStr('-', 'orange', 'apple', 'banana') // orange-apple-banana
  8. joinStr(',', 'orange', 'apple', 'banana') // orange,apple,banana

3. 模拟函数重载
函数重载表示的是在函数名相同的情况下,通过函数形参的不同参数类型或者不同参数个数来定义不同的函数。
我们都知道在JavaScript中是没有函数重载的,主要有以下几点原因:

  • JavaScript是一门弱类型的语言,变量只有在使用时才能确定数据类型,通过形参是无法确定数据类型的。
  • 无法通过函数的参数个数来指定调用不同的函数,函数的参数个数是在函数调用时才确定下来的。
  • 使用函数声明定义的具有相同名称的函数,后者会覆盖前者。 ```javascript function sum(num1, num2) { return num1 + num2 }

function sum(num1, num2, num3) { return num1 + num2 + num3 } // 后定义的sum()函数会覆盖前一个定义的sum() sum(1, 2) // NaN 实际执行的是1 + 2 + undefined = NaN sum(1, 2, 3) // 6

  1. 模拟函数重载:
  2. ```javascript
  3. function sum() {
  4. // 通过call()函数间接调用数组的slice()函数得到函数参数的数组
  5. var arr = Array.prototype.slice.call(arguments)
  6. // 调用数组的reduce()函数进行多个值的求和
  7. return arr.reduce(function (pre, cur) {
  8. return pre + cur
  9. }, 0)
  10. }
  11. sum(1, 2) // 3
  12. sum(1, 2, 3) // 6
  13. sum(1, 2, 3, 4) // 10

构造函数

构造函数与普通函数区别

  • 构造函数的函数名的第一个字母通常会大写。
  • 在函数体内部使用this关键字,表示要生成的对象实例,构造函数并不会显式地返回任何值,而是默认返回“this”。
  • 作为构造函数调用时,必须与new操作符配合使用如果不使用new操作符,则只是一个普通函数,普通函数内部的this会指向window。 ```javascript function Person(name) { console.log(this); // Person {} this.name = name this.sayName = function () { alert(this.name) } } var p = new Person(‘kingx’) console.log(p) // Person {name: “kingx”, sayName: ƒ} Person === Person.prototype.constructor // true

Person(‘kingx’) // 不用new操作符,当作普通函数调用 window.sayName() // ‘kingx’

  1. <a name="g3Mgl"></a>
  2. ## this使用详解
  3. **结论**:在JavaScript中,`**this**`永远指向函数的调用者。<br />1. this指向全局对象<br />当函数没有所属对象而直接调用时,this指向的是全局对象
  4. ```javascript
  5. var value = 10;
  6. var obj = {
  7. value: 100,
  8. method: function () {
  9. var foo = function () { // 匿名函数表达式,
  10. console.log(this.value); // 10
  11. console.log(this); // Window对象
  12. };
  13. foo();
  14. return this.value;
  15. }
  16. };
  1. this指向所属对象
    1. console.log(obj.method()); // 100
  2. this指向对象实例
    1. // 全局变量
    2. var number = 10
    3. function Person() {
    4. // 复写全局变量
    5. number = 20
    6. // 实例变量
    7. this.number = 30
    8. }
    9. // 原型函数
    10. Person.prototype.getNumber = function () {
    11. return this.number
    12. }
    13. // 通过new操作符获取对象的实例
    14. var p = new Person()
    15. console.log(p.getNumber()) // 30
    4. this指向call()函数、apply()函数、bind()函数调用后重新绑定的对象
    使用call()函数、apply()函数、bind()函数都会改变this的指向,call()函数、apply()函数在改变函数的执行主体后,会立即调用该函数;而bind()函数在改变函数的执行主体后,并没有立即调用,而是可以在任何时候调用: ```javascript // 全局变量 var value = 10 var obj = { value: 20 } // 全局匿名函数表达式 var method = function () { console.log(this.value) }

method() // 10 method.call(obj) // 20 method.apply(obj) // 20

var newMethod = method.bind(obj) newMethod() // 20

  1. 在处理DOM事件处理程序中的this时,call()函数、apply()函数、bind()函数显得尤为有用:
  2. ```javascript
  3. var user = {
  4. data: [
  5. { name: "kingx1", age: 11 },
  6. { name: "kingx2", age: 12 }
  7. ],
  8. clickHandler: function (event) {
  9. // 随机生成整数0或1
  10. var randomNum = ((Math.random() * 2 | 0) + 1) - 1
  11. // 从data数组里随机获取name属性和age属性,并输出
  12. console.log(this.data[randomNum].name + " " + this.data[randomNum].age)
  13. }
  14. }
  15. var button = document.getElementById('btn')
  16. // this指向的是button对象,而不是user对象,而button对象没有data属性,为undefined,从而抛出异常。
  17. button.onclick = user.clickHandler // TypeError: Cannot read property '1' of undefined
  18. button.onclick = user.clickHandler.bind(user); // kingx2 43 kingx1 37

5. 闭包中的this
函数的this变量只能被自身访问,其内部函数无法访问。因此在遇到闭包时,闭包内部的this关键字无法访问到外部函数的this变量。

  1. var user = {
  2. sport: 'basketball',
  3. data: [
  4. { name: "kingx1", age: 11 },
  5. { name: "kingx2", age: 12 }
  6. ],
  7. clickHandler: function () {
  8. // 此时的this指向的是user对象
  9. var _this = this
  10. this.data.forEach(function (person) { // forEach循环实际是一个匿名函数
  11. console.log(this) // [object Window]
  12. console.log(_this) // user对象
  13. console.log(person.name + ' is playing ' + this.sport)
  14. })
  15. }
  16. }
  17. user.clickHandler()

6. Vuex解决this指向丢失

  1. class Store {
  2. constructor(options = {}) {
  3. // 构造器中的this指向实例
  4. // 实例属性
  5. this._actions = Object.create(null);
  6. // 缓存this
  7. const store = this
  8. // 因为this指向实例,所以可以通过原型链找到dispatch,commit这两个方法
  9. const {
  10. dispatch,
  11. commit
  12. } = this
  13. // 在实例上绑定了两个方法dispatch、commit,解决了实例上dispatch、commit的this指向问题
  14. this.dispatch = function boundDispatch(type, payload) {
  15. console.log('我是实例上的dispatch');
  16. return dispatch.call(store, type, payload)
  17. }
  18. this.commit = function boundCommit(type, payload, options) {
  19. return commit.call(store, type, payload, options)
  20. }
  21. }
  22. // 原型方法,绑在类Store原型对象上,供实例使用。
  23. dispatch() {
  24. // 类中的方法默认开启了局部的严格模式,被实例调用this就指向实例,被全局调用this为undefined
  25. // 但是在constructor中被调用时,被改变了this指向
  26. console.log('我是原型上的dispatch', this);
  27. }
  28. commit() {
  29. console.log('commit', this);
  30. }
  31. // 原型属性 (测试用)
  32. test = 1
  33. }
  34. const store = new Store();
  35. store.dispatch(); //通过实例调用,输出结果 this 是什么呢?
  36. const {
  37. dispatch,
  38. commit
  39. } = store;
  40. dispatch(); // 在全局环境中直接调用从实例身上解构出来的方法(不是调用实例上的原型方法dispatch)
  41. commit();
  1. // 简化版本:使用bind关键字重新绑定了getName()函数在调用时内部的this,使其指向实例p
  2. class Person {
  3. constructor(name) {
  4. this.name = name;
  5. // 构造器中的this指向当前实例
  6. this.getName = this.getName.bind(this);
  7. // 或者用箭头函数
  8. // 箭头函数内部的this总是指向定义时所在的对象。箭头函数位于构造函数内部,它定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。
  9. this.getName = () => this;
  10. }
  11. getName() {
  12. return this.name;
  13. }
  14. }
  15. const p = new Person('king');
  16. let { getName } = p;
  17. getName(); // king

加深对this的理解: 这题目太🐂了

  1. function f(k) {
  2. this.m = k;
  3. return this;
  4. }
  5. // 先执行f(1),因为f()函数的调用没有所属对象,所以this指向window,此时window.m=1,返回window
  6. // 再将返回值“window”赋值给变量m,因为m是全局变量,所以window.m=window
  7. var m = f(1); // window
  8. // 先执行f(2),此时window.m=2,覆盖了window.m=window,最终window.m=2
  9. // 再将返回值“window”赋值给全局变量n,最终window.n=window
  10. var n = f(2); // window
  11. console.log(m.m); // undefined m.m=(window.m).m,此时window.m=2即2.m,
  12. console.log(n.m); // 2 n.m=(window.n).m=window.m=2

闭包

执行上下文环境

JavaScript每段代码的执行都会存在于一个执行上下文环境中,而任何一个执行上下文环境都会存在于整体的执行上下文环境中。根据栈先进后出的特点,全局环境产生的执行上下文环境会最先压入栈中,存在于栈底。当新的函数进行调用时,会产生的新的执行上下文环境,也会压入栈中。当函数调用完成后,这个上下文环境及其中的数据都会被销毁,并弹出栈,从而进入之前的执行上下文环境中。处于活跃状态的执行上下文环境只能同时有一个:
image.png

  1. var a = 10 // 1.进入全局执行上下文环境
  2. var fn = function (x) {
  3. var c = 10
  4. console.log(c + x)
  5. }
  6. var bar = function (y) {
  7. var b = 5
  8. fn(y + b) // 3.进入fn()函数执行上下文环境
  9. }
  10. bar(20) // 2.进入bar()函数执行上下文环境
从第1行代码开始,进入全局执行上下文环境,此时执行上下文环境中只存在全局执行上下文环境。 image.png
当代码执行到第10行时,调用bar()函数,进入bar()函数执行上下文环境中。 image.png
进入bar()函数中,执行到第8行时,调用fn()函数,进入fn()函数执行上下文环境中。 image.png
进入fn()函数中,执行完第5行代码后,fn()函数执行上下文环境将会被销毁,从而弹出栈。 image.png
fn()函数执行上下文环境被销毁后,回到bar()函数执行上下文环境中,执行完第9行代码后,bar()函数执行上下文环境也将被销毁,从而弹出栈。 image.png
最后全局上下文环境执行完毕,栈被清空,流程执行结束。

有另外一种情况,虽然代码执行完毕,但执行上下文环境却被无法干净地销毁,这就是闭包。

闭包的概念

官方通用的解释:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。(函数套函数,外部函数返回内部函数,内部函数访问外部函数的变量)
特点

  1. 函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
  2. 闭包作为一个函数返回时,其执行上下文环境不会被销毁,仍处于执行上下文环境中。
    1. function fn() {
    2. var max = 10
    3. return function bar(x) {
    4. if (x > max) {
    5. console.log(x)
    6. }
    7. }
    8. }
    9. var f1 = fn()
    10. f1(11) // 11
    | 代码开始执行后,生成全局上下文环境,并将其压入栈中 | image.png | | —- | —- | | 代码执行到第9行时,进入fn()函数中,生成fn()函数执行上下文环境,并将其压入栈中。fn函数执行完毕返回一个bar()函数,并将其赋给变量f1。 | image.png | | 当代码执行到第10行时,调用f1()函数,因为f1()函数中包含了对max变量的引用,而max变量是存在于外部函数fn()中的,所以fn()函数执行上下文环境并不会被直接销毁,依然存在于执行上下文环境中。 | image.png | | 等到第10行代码执行结束后,bar()函数执行完毕,bar()函数执行上下文环境才会被销毁,同时因为max变量引用会被释放,fn()函数执行上下文环境也一同被销毁。最后全局上下文环境执行完毕,栈被清空,流程执行结束。 | |

闭包的用途

1. 结果缓存

假如有一个处理很耗时的函数对象,每次调用都会消耗很长时间。我们可以将其处理结果在内存中缓存起来。这样在执行代码时,如果内存中有,则直接返回;如果内存中没有,则调用函数进行计算,更新缓存并返回结果。因为闭包不会释放外部变量的引用,所以能将外部变量值缓存在内存中

  1. var cachedBox = (function () {
  2. // 缓存的容器
  3. var cache = {}
  4. return {
  5. searchBox: function (id) {
  6. // 如果在内存中,则直接返回
  7. if (id in cache) {
  8. return '查找的结果为:' + cache[id]
  9. }
  10. // 经过一段很耗时的dealFn()函数处理
  11. var result = dealFn(id)
  12. // 更新缓存的结果
  13. cache[id] = result
  14. // 返回计算的结果
  15. return '查找的结果为:' + result
  16. }
  17. }
  18. })()
  19. // 处理很耗时的函数
  20. function dealFn(id) {
  21. ······
  22. ·········
  23. console.log('这是一段很耗时的操作')
  24. return id
  25. }
  26. // 两次调用searchBox()函数
  27. console.log(cachedBox.searchBox(1)) // 这是一段很耗时的操作 // 查找的结果为:1
  28. // 由于第一次已经将结果更新到cache对象中,并且该对象引用并未被回收,直接从缓存中读取
  29. // 这样并没有执行很耗时的函数,还间接提高了执行效率。
  30. console.log(cachedBox.searchBox(1)) // 查找的结果为:1

2. 封装

在JavaScript中提倡的模块化思想是希望将具有一定特征的属性封装到一起,只需要对外暴露对应的函数,并不关心内部逻辑的实现
例如,我们可以借助数组实现一个栈,只对外暴露出表示入栈和出栈的push()函数和pop()函数,以及表示栈长度的size()函数。

  1. var stack = (function () {
  2. // 使用数组模仿栈的实现
  3. var arr = []
  4. // 栈
  5. return {
  6. push: function (value) {
  7. arr.push(value)
  8. },
  9. pop: function () {
  10. return arr.pop()
  11. },
  12. size: function () {
  13. return arr.length
  14. }
  15. }
  16. })()
  17. stack.push('abc')
  18. stack.push('def')
  19. console.log(stack.size()) // 2
  20. stack.pop()
  21. console.log(stack.size()) // 1

优缺点

优点:

  • 保护函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
  • 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。

缺点:

  • 消耗内存:通常来说,函数的活动对象会随着执行上下文环境一起被销毁,但是,由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,闭包所存在的最大的一个问题就是消耗内存,如果闭包使用越来越多,内存消耗将越来越大。
  • 泄漏内存:在IE9之前,如果闭包的作用域链中存在DOM对象,则意味着该DOM对象无法被销毁,造成内存泄漏。 ```javascript // 该element元素在网页关闭之前会一直存在于内存之中,不会被释放。 function closure() { var element = document.getElementById(“elementID”) element.onclick = function () { console.log(element.id) } }

// 解决方法: function closure() { var element = document.getElementById(“elementID”); // 使用临时变量存储 var id = element.id; element.onclick = function () { console.log(id); }; // 手动将元素设置为null element = null; } ```