JavaScript(8)-闭包-原型

2021/9/10 20:04:58

本文主要是介绍JavaScript(8)-闭包-原型,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、闭包 closure

1. 复习作用域

作用域是可访问变量的有效范围。

  • 全局作用域

作用于所有代码执行的环境 (整个script标签内部) 或独立的js文件。

  • 局部作用域(函数作用域)

作用于函数内的代码环境,就是局部作用域。

  • 全局变量

在全局作用域下(函数外部)声明的变量叫做全局变量。

  1. 网页中所有脚本和函数均可使用全局变量
  2. 如果变量在函数内没有声明(没有使用 var 关键字),该变量为全局变量
  • 局部变量

在局部作用域下(函数内部)声明的变量叫做局部变量。

  1. 局部变量只能在该函数内部使用。
  2. 局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁。
  3. 因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。
  4. 函数参数只在函数内起作用,是局部变量。
  • 全局变量/函数,可以覆盖window对象的变量/函数。
// 全局变量/函数,可以覆盖window对象的变量/函数。
window.variable = 'window对象的变量';
var variable = '全局变量';
console.log(variable); // 返回:全局变量
  • 局部变量/函数,可以覆盖window对象的变量和全局变量/window对象的函数和全局变量的函数。
// 局部变量/函数,可以覆盖window对象的变量和全局变量/window对象的函数和全局变量的函数。
function test4() {
  var variable = '局部变量';
  console.log(variable); // 返回:局部变量
}
test4();

2. 外部怎么读取函数内的局部变量?

出于种种原因,我们有时候需要得到函数内的局部变量。

但是,前面已经说过了,正常情况下,这是办不到的,

只有通过变通方法才能实现:那就是在函数的内部,再定义一个函数。

function fn1() {  
  var num = 10;
  function fn2() {
    console.log(num);  // 10
  }
}

在上面的代码中,函数fn2就被包括在函数fn1内部。这时fn1内部的所有局部变量,对fn2都是可见的。但是反过来就不行,fn2内部的局部变量,对fn1就是不可见的。

这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然fn2可以读取fn1中的局部变量,那么只要把fn2作为返回值,我们不就可以在fn1外部读取它的内部变量了嘛。

function fn1() {  // fn1函数就是闭包
  var num = 10;
  function fn2() {
    return num;
  }
  return fn2;  // fn2没有调用不执行,fn1的返回值是fn2函数
}
var f = fn1(); // 调用fn1,得到返回值--fn2函数,把它保存到变量f中
f();  // 此时f保存的是fn2函数,调用该函数,就能得到fn2的返回值--num
console.log(f());  // 10

3. 闭包的概念

闭包就是能够读取其他函数内部变量函数。(变量所在的函数就是闭包函数)

例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成——“定义在一个函数内部的函数“。

在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

注意:

  1. 闭包不是函数套函数

    是因为需要局部变量,所以才把 num 放在一个函数里,如果不把 num 放在一个函数里,num 就是一个全局变量了,达不到使用闭包的目的——隐藏变量

  2. 闭包中的函数是可以没有return语句

    因为如果不 return,你就无法使用这个闭包。把 return fn2 改成 window.fn2 = fn2 也是一样的,只要让外面可以访问到这个 fn2 函数就行了。

    所以 return fn2 只是为了 fn2 能被使用,也跟闭包无关。

4. 闭包的作用

闭包可以用在许多地方。它的最大用处有两个:

  1. 可以读取函数内部的变量(延伸变量的作用范围,但仅能读取访问,不能修改)
  2. 让这些变量的值始终保持在内存中

5. 闭包的使用案例

① 点击 li 输出当前 li 的索引号

<ul class="nav">
  <li>项目1</li>
  <li>项目2</li>
  <li>项目3</li>
  <li>项目4</li>
</ul>
  • 不使用闭包时,可以利用动态添加属性的方式:
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
  // 2. 所以给每个li动态的添加属性
  lis[i].index = i;
  lis[i].onclick = function () {
    // 1. 因为点击事件函数是异步任务,点击了才会执行,但是for循环是同步任务,会立刻执行,所以会先循环完一遍,等到点击前i就已经是3了,点击后i变成4,所以不管点哪个都会输出4
    // console.log(i); 
    console.log(this.index); // this指向的是调用函数的对象,也就是点击的那一个
  }
}
  • 利用闭包的方式得到当前li的索引号
// 立即执行函数也称为小闭包 因为立即执行函数里面的任何一个函数都可以使用它的i这个变量
for (var i = 0; i < lis.length; i++) {   // 1. 循环的i是几
  // 每次循环都会创建一个立即执行函数
  (function (i) {   // 3. 形参i接收传来的实参i
    // 点击事件函数访问了立即执行函数的参数i,立即执行函数就是个闭包函数
    lis[i].onclick = function () {   // 4. 哪一个被点击了  
      console.log(i);   // 5. 就输出哪一个的索引号
    }
  })(i)   // 2. 就把i作为实参传给立即执行函数形参i
}

② 3秒钟之后,打印上述案例中所有 li 元素的内容

var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(lis[i].innerHTML);  // 定时器函数使用了立即执行函数的参数i,立即执行函数就是个闭包函数
    }, 3000)
  })(i);
}
// 若不把定时器函数放在立即执行函数内部,则会报错 原因同上,定时器也是异步任务,循环完i已经是3了,到定时器触发时i变为4,索引号4拿不到内容,所以报错

③ 计算打车价格

  • 打车起步价13(3公里内), 之后每多一公里增加5块钱,用户输入公里数就可以计算打车价格

  • 如果有拥堵情况,总价格多收取10块钱拥堵费

// 函数要声明及调用,这里方便起见,写成一个立即执行函数  price和yd函数使用了立即执行函数里的局部变量,所以立即执行函数就是个闭包函数
var car = (function () {
  var start = 13; // 起步价 局部变量
  var total = 0;  // 总价 局部变量
  return {  // 要返回2个函数,则把这2个函数放在一个对象里返回
    // 正常的总价
    price: function (n) {
      if (n <= 3) {
        total = start;
      } else {
        total = start + (n - 3) * 5
      }
      return total;
    },
    // 拥堵之后的费用
    yd: function (flag) {
      return flag ? total + 10 : total;
    }
  }
})();
console.log(car.price(5));  // 23
console.log(car.yd(true));  // 33

console.log(car.price(1));  // 13
console.log(car.yd(false));  // 13

二、原型

原型和原型链是JavaScript进阶重要的概念,尤其在插件开发过程中是不能绕过的知识点。

了解原型我们从以下这个例子开始:

内置对象Array做一个数字排序

var arr1 = [1, 0, 0, 8, 6];
var arr2 = [1, 0, 0, 8, 6, 1, 1];
// arr1升序排序
arr1.sort(function (a, b) {
  return a - b;
});
// arr2升序排序
arr2.sort(function (a, b) {
  return a - b;
});
console.log(arr1);  // [0, 0, 1, 6, 8]
console.log(arr2);  // [0, 0, 1, 1, 1, 6, 8]

console.log(arr1 === arr2);  // false 因为arr1和arr2中的元素和元素个数不同。
console.log(arr1.sort === arr2.sort);  //true 因为arr1和arr2使用的是相同的sort方法,sort方法是arr1和arr2的公共方法

这个公共的sort方法不是arr1和arr2的方法,而是Array的方法;

我们通过Array创建了arr1和arr2这两个数组对象,此时arr1和arr2者两个数组对象会从Array继承到sort方法。

下面我们尝试给arr1增加一个getSum的方法:

arr1.getSum = function () {
  var sum = 0;
  for (var i = 0; i < this.length; i++) {
    sum += this[i];
  }
  return sum;
}
// arr1可以正常调用getSum这个方法
var a = arr1.getSum();
console.log(a);  // 15  
// 但是arr2不能调用 因为arr2里没有getSum()方法
var b = arr2.getSum();
console.log(b);  // 报错:arr2.getSum is not a function 

由此可知 getSum() 不是公共方法,意味着 getSum() 在Array中没有。

如果希望arr2也能有getSum()求和方法,目前我们有2种做法:

  1. 给arr2编写一个与arr1一样的求和方法,缺点是要编写一堆重复的代码
  2. 将getSum()求和方法交给Array,成为公共方法

那么,如何将getSum()求和方法交给Array呢?

答案:将实例属性/方法定义为原型属性/方法——Array.prototype.属性/方法

Array.prototype.getSum = function () {
  var sum = 0;
  for (var i = 0; i <this.length; i++) {
    sum += this[i];
  }
  return sum;
}
console.log(arr1.getSum());  // 15
console.log(arr2.getSum());  // 17

要想知道 prototype 是什么?就需要从对象的创建开始了解:

1. 创建对象的方式

方式1:使用字面量创建对象

var Person = {
  name: 'zhangsan',
  age: 20,
  address: '西安',
  test1: function () {
    console.log('Person对象中的方法');
  }
}
console.log(Person.name);  // zhangsan
Person.test1();  // Person对象中的方法
  • 缺点:使用字面量方式创建的对象都是具体的一个对象,当需要很多个具体对象的时候,就会产生大量的代码重复

我们可以把对象中一些公共的属性和方法抽取出来,封装到一个函数里面

方式2:利用构造函数创建对象

// 先声明构造函数
function Man(name,age,address){
  this.name=name;
  this.age=age;
  this.address=address;
  this.test = function(){
    console.log('我是Man对象中的方法');
  }
}
// 再使用关键字new实例化对象
var zs= new Man('张三',23,'西安');
var ls= new Man('李四',25,'北京');

// 此时实例化对象都可以使用构造函数Man里面的属性和方法
console.log(zs.address);  // 西安
zs.test();  // 我是Man对象中的方法
console.log(ls.age);  // 25   
ls.test();  // 我是Man对象中的方法

问题:那么为什么构造函数创建出来的对象可以使用它的属性和方法呢?

答案:对象实例和它的构造函数之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造函数中找到这属性和方法。

当我们通过构造函数创建对象的时候,实际上是创建了2个对象:

  • 第一个对象就是用来调用属性和方法的实例对象[zs,ls]
  • 第二个对象就是构造函数.prototype得到的对象【原型对象】— 构造函数

2. 原型对象prototype

每个构造函数上面都有一个属性(prototype),指向了函数的原型对象(Man.prototype)

即使你只定义了一个空函数,也存在一个prototype的属性。

console.log(Man.prototype); // {constructor: ƒ}

3. 对象原型(__proto__)

每个实例对象上面都有一个隐式原型(proto),指向了函数的原型对象

console.log(zs.__proto__); // {constructor: ƒ}
  • 对象的__proto__属性 和 构造函数的prototype属性 都指向了构造函数的原型对象,他们是等价的
console.log(zs.__proto__ === Man.prototype); // true

实例访问属性或者方法的时候,遵循以下原则:

  • 如果实例上面存在,就用实例本身的属性和方法。
  • 如果实例上面不存在,就会顺着__proto__的指向一直往上查找,查找就停止。
// 下面给实例对象zs自身增加test方法:
zs.test = function(){
  console.log('我是zs实例对象的方法');
}
// zs和构造函数的原型对象(Man.prototype)上面都有test方法,则zs实例调用自身的test方法
zs.test();  // 我是zs实例对象的方法   
// ls自身没有test方法,会顺着ls对象的__proto__属性指向的原型找,看有没有test方法,结果找到了,则ls调用原型上面的test方法。
ls.test();  // 我是Man对象中的方法 

4. constructor 构造函数

对象原型( __proto__)和构造函数(prototype)原型对象里面都有一个属性 :constructor 属性。

constructor 我们称为构造函数,因为它指回构造函数本身

constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

console.log(Man.prototype.constructor);  // 输出Man构造函数
console.log(zs.__proto__.constructor);   // 输出Man构造函数

5. 三角关系

在这里插入图片描述

三、原型链

  • 只要是对象,就有__proto__原型,指向构造函数的原型对象
  • 构造函数的原型对象也是一个对象,也有__proto__属性,指向它的构造函数的原型对象
  • 这样一层一层往上找就形成了原型链。
  • Object的原型对象是原型链的最顶端。

Man原型对象prototype也是个对象,里面也有__proto__原型,指向的是Object.prototype原型对象,Object.prototype原型对象是由Object构造函数创造出来的;

Object.prototype原型对象里面的__proto__原型,指向为null

console.log(Man.prototype);  // 里面也有一个__proto__,指向Object原型对象
console.log(Man.prototype.__proto__ === Object.prototype);  // true
console.log(Object.prototype.__proto__);  // null

在这里插入图片描述



这篇关于JavaScript(8)-闭包-原型的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程