第五章:函数—上-灵析社区

懒人学前端

一、什么是闭包?

MDN 定义如下:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。

闭包的特性

如下面的例子:

function person() {
	var name = "John";
	function showName() {
		console.log(name); // 'John'
	}
	return showName;
}
const getPerson = person();
getPerson();

showName() 函数中并没有定义 name 变量,但是却可以访问到外部函数中 name 变量的值,这就是利用了闭包的特性,即嵌套函数可访问声明于它们外部作用域的变量。

词法作用域

词法作用域是静态作用域,通俗讲就是它可以访问到变量的范围。

var name = 'John';

function greeting() { 
    let message = 'Hi';
    console.log(message + ' '+ name);
}

在上面的例子中:

  • 变量 name 是个全局变量,它可以在任意地方访问到,包括函数 greeting()
  • 变量 message 是个局部变量,只能在函数 greeting() 中访问到

闭包在循环中

下面这段代码很常见:

for (var index = 1; index <= 3; index++) {
    setTimeout(function () {
        console.log('after ' + index + ' second(s):' + index);
    }, index * 1000);
}

这里输出如下:

// after 4 second(s):4
// after 4 second(s):4
// after 4 second(s):4

这是因为,var 定义的变量是个全局变量,当 setTimeout 调用时,全局变量index 的值被修改为 4,所以每次打印中获取到的 index 值是 4。

针对上面的问题,可以使用闭包和立即执行函数(IIFE)搭配解决。

闭包

for (var index = 1; index <= 3; index++) {
    (function (index) {
        setTimeout(function () {
            console.log('after ' + index + ' second(s):' + index);
        }, index * 1000);
    })(index);
}

// 输出
// after 1 second(s):1
// after 2 second(s):2
// after 3 second(s):3

上面我们在 setTimeout 函数外使用了立即执行函数,立即执行函数 index 是局部变量,与全局变量 index 处于不同的作用域,此时 setTimeout 函数中能拿到立即执行函数中的 index 变量的值,因此能够得到正确的输出结果。

闭包的应用

缓存变量的值

闭包和函数内传入的值有关联。下面的代码中 createInc 函数调用后,返回一个箭头函数,箭头函数中的 index startValue 是外部变量,箭头函数内可以拿到两个变量的值,并且可以对变量的值进行修改。

我们可以使用闭包的特性缓存变量的值:

function createInc(startValue) {  
    let index = -1;  
    return (step) => {    
        startValue += step;    
        index++;    
        return [index, startValue];  
     };
}
const inc = createInc(5);
console.log(inc(2)); // [0, 7]
console.log(inc(2)); // [1, 9]
console.log(inc(2)); // [2, 11]

用闭包模拟私有方法

我们可以使用闭包来模拟私有方法。私有方法可以限制对代码的访问,而且可以用于管理全局变量命名,避免扰乱公共代码。

let Counter = (function() {
  let privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1

观察上面代码,Counter 函数立即调用之后,返回一个对象,其中包含三个函数: incrementdecrementvalue,它们都能访问 Counter 函数内部的私有项:privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

小练习

下面代码输出什么?

const foo = (function() {
    var v = 0
    return () => {
        return v++
    }
}())
for (let i = 0; i < 10; i++) {
    foo()
}
console.log(foo())

答案:10。因为 foo 是个立即执行函数,返回的是一个箭头函数。当的 foo 调用时,就是箭头函数调用,箭头函数执行了 10 次,其中变量 v 自增到 9,之所以输出 10 是因为最后一次 console.log 中的调用。

二、this 的指向有哪些?

函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。

this 的指向有以下七种:

全局上下文

this 指向全局对象

console.log(this === window); // true

a = 37;
console.log(window.a); // 37

函数上下文

this 指向取决于函数被调用的方式:

  • 作为对象的方法调用,this 指向该对象
  • 作为普通函数调用

严格模式下,指向全局对象,浏览器中就是 window

非严格模式下,为 undefined

  • callapplybind 调用,this 指向绑定的对象
  • 作为构造函数调用,如使用 newthis 指向新的对象
function f1(){
  return this;
}
//在浏览器中:this 指向全局对象 window
f1() === window;  

//在 Node 中:this 指向 global
f1() === global;

// 严格模式下:this 指向 undefined
function f2(){
  "use strict"; 
  return this;
}

f2() === undefined; // true

// call 方法:this 指向传入的指定对象 o
function add(c, d) {
  return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
add.call(o, 5, 7); // 16

类上下文

this 指向类。在类的构造函数中,this 是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中:

class Car {
  constructor() {
      // 使用 bind() 方法改变 this 指向
    this.sayBye = this.sayBye.bind(this);
  }
  sayHi() {
    console.log(`Hello from ${this.name}`);
  }
  sayBye() {
    console.log(`Bye from ${this.name}`);
  }
  get name() {
    return 'Ferrari';
  }
}

class Bird {
  get name() {
    return 'Tweety';
  }
}

const car = new Car();
const bird = new Bird();

// this 指向调用者
car.sayHi(); // Hello from Ferrari
bird.sayHi = car.sayHi;
bird.sayHi(); // Hello from Tweety

// bind() 方法改变了 this 指向,this 指向类 Car
bird.sayBye = car.sayBye;
bird.sayBye();  // Bye from Ferrari

箭头函数

this与封闭词法环境的 this 保持一致。在全局代码中,它将被设置为全局对象。

var window = this;
var foo = (() => this);
console.log(foo() === window); // true

原型链中的 this

如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象。

var o = {
  f: function() {
    return this.a + this.b;
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;

console.log(p.f()); // 5

作为一个 DOM 事件处理函数

当函数被用作事件处理函数时,它的 this 指向触发事件的元素。


function bluify(e) {
    console.log(this === e.currentTarget); // true
    this.style.backgroundColor = 'blue'
}

// 获取 id 为 test 的 button
let testBtn = document.getElementsById('test');

// 将 bluify 作为元素的点击监听函数,当元素被点击时,就会变成蓝色
testBtn.addEventListener('click', bluify, false);

getter 与 setter 中的 this

用作 getter setter 的函数都会把 this 绑定到设置或获取属性的对象。

var o = {
  a: 1,
  b: 2,
  get sum() {
    return (this.a + this.b);
  }
};

console.log(o.sum); // 输出 3

小练习

给出下面代码的输出结果:

var length = 10;
function fn() {
  return this.length + 1;
}
var obj = {
  length: 5,
  test1: function() {
    return fn();
  }
};
obj.test2 = fn;

console.log(obj.test1.call()); // (1)
console.log(obj.test1()); // (2)
console.log(obj.test2.call()); // (3)
console.log(obj.test2()); // (4)

答案:输出如下:

  • 1 处为:11, this 指向 window, 因为 call 方法没有执行具体要绑定的对象。
  • 2 处为:11,this 指向 window,返回的 fn 函数的 this 指向 window
  • 3 处为:11,原因同 1 处
  • 4 处为: 6,this 指向 obj,此处函数作为对象 obj 的方法调用

三、类数组的转化方式有哪些?

类数组不是数组,是对象。

例如 document.getElementsByTagName() 返回的 NodeListarguments 等 JavaScript 对象,有与数组相似的行为,但它们并不共享数组的所有方法。arguments 对象提供了 length 属性,但没有实现如 forEach() 等数组方法。不能直接在类数组对象上调用数组方法。

在函数中,arguments 对象代表函数实参。arguments 对象是一个类数组对象,类数组不是数组,因此它不是 Array 类型的实例,它是 Object 类型的实例。

function add (one, two) {
    console.log(arguments);
    console.log(Array.isArray(arguments));
    console.log(Object.prototype.toString.call(arguments));
}
add(1, 2); 

结果如下:

arguments 特点

arguments 既然是类数组对象,它有两个特点:

  • 可以通过 [] 获取参数, 如 arguments[0]
  • 可以使用 length 属性获取实参的个数。如 arguments.length
function add() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

console.log(add(1, 2)); // 3

类数组转成数组

我们可以使用三种方式将类数组转成数组:

  • 扩展运算符
  • Array.from()
  • slice(),使用以下两种方式都可以:

Array.prototype.slice.call(arguments)

[].slice.call(arguments);

// 扩展运算符
function checkArgs() {
	return [...arguments];
};

let result = checkArgs(1, 2);
console.log(result); // [1, 2]

// Array.from()
function checkArgs() {
	return Array.from(arguments);
};

let result1 = checkArgs(1, 2);
console.log(result1); // [1, 2]

// slice()
function checkArgs() {
	return Array.prototype.slice.call(arguments);
};

let result2 = checkArgs(1, 2);
console.log(result2); // [1, 2]


阅读量:176

点赞量:0

收藏量:0