原型

在ES6之前,javascript是没有“类”的概念的,这是由于js诞生就是为了响应用户的交互,如果有了“类”,就成了一种完整的面向对象编程语言了。但是,js中都是对象,必须有一种机制,将所有对象联系起来,所以“继承”还是很有必要的。

new运算符

我们都知道,C和Java都是通过new来生成实例的,于是js也使用了这个命令。在js中,new后面跟的不是原型对象,而是构造函数。

下面我们看段代码:

function People(name) {
  this.name = name;
  this.say = function() {
    console.log(`I'm ${this.name}`)
  }
}
let linxun = new People('linxun');
linxun.say()  // I'm linxun

注意:this指的是新创建的实例对象

上面的例子中,People是构造函数,其中有一个name属性和一个say方法,每个实例的name属性是不一样的,say方法却是相同的,所以用new生成实例对象有一个很大的缺陷,就是不能实现共享属性和方法,每一个实例对象的属性和方法都是自身独有的,这是对资源极大的浪费。

扯了这么多,终于到重点了!

prototype属性的引入

这个属性(只要函数才有)包含了一个对象(以下称为“prototype对象”),所有实例需要共享的属性和方法,都放在这个对象上。而那些不需要共享的,实例独有的属性和方法就放在构造函数里面。实例创建之后,会自动引用prototype对象中的属性和方法。

还是刚才那个例子,我们改写一下:

function People(name) {
  this.name = name;
}
People.prototype = {
  say: function() {
    console.log(`I'm ${this.name}`);
  }
}
let linxun = new People('linxun');
let zyf = new People('zyf);
linxun.say()  // I'm linxun
zyf.say()     // I'm zyf

现在say方法就是所有实例共享的了,如果修改了它,所有的实例对象都会受影响。

People.prototype.say = function() {
  console.log('我被修改了');
}
linxun.say()  // 我被修改了
zyf.say()     // 我被修改了

原型链的实现

引用《JavaScript权威指南》中的一段描述

Every JavaScript object has a second JavaScript object (or null ,but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.

我的理解就是每个js对象(null除外)都有一个原型对象,并且从原型对象上继承属性和方法。

既然有这么一个原型对象,那么我们的对象怎么和原型对象对应起来呢?对象的 __proto__ (以下称为“原型对象”) 属性就指向它构造函数的prototype(以下称为“原型”),而对象的原型本身也是一个对象,也有 __proto__ 属性,这就形成了原型链。原型链的最顶端是 Object.prototype ,它的 __proto__ 指向null(ECMA规定的,避免无限循环)。

从前面我们知道,函数创建的时候就拥有了prototype属性,我们创建实例的时候依靠它实现属性和方法的继承。

这里插入一下,new操作符到底做了什么事情?

  1. 创建一个空对象 var obj = {}
  2. 将创建对象的 __proto__ 指向构造函数的prototype obj.__proto__ = 构造函数.protootype
  3. 将对象内部的this指向新创建的对象 构造函数.call(obj)

还是看代码吧:

var obj1 = {}
var obj2 = new Object();
console.log(obj1.__proto__ === Object.prototype);   // true
console.log(obj2.__proto__ === Object.prototype);   // true
console.log(Object.prototype.__proto__ === null);   // true

还是有点懵?看张图吧,我轻易不放出来。其实只是因为我很懒不想打这么多字……

属性或方法的查找

当查找一个对象的属性或方法时,js会向上遍历原型链,先在自身查找,没找到就到它原型对象上找,依次向上查找,直到原型链的最顶端Object.prototype,如果仍然没有找到,就返回undefined(不存在该属性)或者抛出一个错误(不存在该方法)。

闭包

闭包是js的一大特色,也是一大难点。下面就是我对闭包的理解。

变量的作用域

说起闭包,就不得不先了解下js特殊的变量作用域。js中变量作用域就两种:全局变量局部变量

  • 全局变量:在任何地方都能访问
  • 局部变量:只有在特定的地方才能访问

举个例子:
var a = 10;
function test() {
var b = ‘hello world’;
console.log(a, b);
}
test(); // 10, hello world
console.log(a, b); // 10, error: b is not defined

上面例子中的 a 就是全局变量,在哪里都能访问;b 就是局部变量,只有在 test 函数中才能访问。
注意:声明 b 变量时 var 不能省略,否则就声明了一个全局变量

如何从函数外部读取局部变量

在很多时候,我们需要得到函数内部的局部变量,在正常情况下这是做不到的。我们可以这样,在函数中再声明一个函数。下面代码中,函数fun2中是可以访问fun1函数中的局部变量的,而反过来就不行。这就是js特殊的链式作用域,在下面我们会讲到这个问题。

function fun1() {
  var a = 10;
  function fun2() {
    console.log(a);   // 10
  }
}

既然函数fun2可以读取函数fun1中的局部变量,如果我们将fun2作为fun1的返回值返回出去,那么我们不就可以在外面读取函数fun1的局部变量了吗?

function fun1() {
  var a = 10;
  return function() {
    console.log(a);
  }
}
var result = fun1();
result();   // 10

上面代码中,声明一个全局变量result来接收fun1的返回值(fun2),意味着fun2将一直存在,而fun1作为fun2的依赖函数,也将一直存在作用域中。如果将result销毁,自然也就访问不到fun1中的局部变量了。

闭包的概念

上面例子中的fun2就是闭包。参考阮一峰阮大神的文章:学习javascript闭包

闭包就是能够读取其他函数内部变量的函数

我的理解是:闭包是指有权访问另一个函数作用域中变量的函数。在A函数中声明定义B函数,且B函数中使用了A函数中的局部变量,这就是闭包。即使A函数调用完毕,B函数依然可以访问那个局部变量。如果A函数没有执行完,B函数中使用的局部变量的值是其当前值,如果A函数执行完毕,B函数中使用的局部变量就是最终值。

闭包的作用

讲了那么多,闭包究竟有什么用呢?

  • 上面例子中的,读取函数内部的变量
  • 让一个变量常驻内存,上面代码中的变量a就已经常驻内存了,只有通过我们手动销毁。

IIFE

下面看一个例子:假设页面上有五个按钮,对每个按钮添加点击事件

var ele = document.getElementsByTagName('button');
for(var i=0, l=ele.length; i<l; i++) {
  ele[i].addEventListener('click', function() {
    console.log(i);
  });
}

上面代码打印出了五个5,而不是我们期待的0,1,2,3,4;因为 i 是全局变量自始至终就只有一个 i,点击事件执行时,循环早已经执行完毕,它闭包到的永远是 i 最后的值。而如果改写成下面这样:

var ele = document.getElementsByTagName('button');
for(var i=0, l=ele.length; i<l; i++) {
  (function(j) {
    ele[i].addEventListener('click', function() {
      console.log(j);
    });
  })(i)
}

上面代码就打印出我们期待的0,1,2,3,4了;使用了IIFE立即执行函数创建了五个 i 的作用域,每个作用域中的 i 相互独立不受影响,避免了 i 的共用。

闭包的缺陷

前面也提到了,闭包会使得函数中的变量一直保存在内存中,所以不能滥用闭包,否则会造成性能问题。解决方法是:在不需要该变量时手动清除。

作用域和作用域链

js中变量的作用域分为全局作用域和局部作用域,作用域(执行环境)就是变量和函数的可访问范围。某个执行环境中所有代码执行完毕后,该环境就会被销毁,保存在其中的所有变量和函数定义也会被销毁,全局执行环境直到应用程序退出——如关闭网页或浏览器——时才会被销毁。

在任何地方都能访问到的对象拥有全局作用域,一般有以下几种情况:

  1. 最外层函数和在最外层函数外面定义的变量
  2. 未申明直接赋值的变量自动声明为全局作用域
  3. 所有window对象的属性

局部作用域只在固定的代码片段可以访问,最常见的如函数内部

ES6之前,js没有块级作用域 ( IIFE 实现块级作用域 let:es6)

作用域链:函数创建时有一个内部属性[[Scope]],这个内部属性包含了函数创建时作用域中对象的集合,这个集合决定了哪些数据能被访问。

  1. 函数创建的时候,作用域链会填入一个全局对象,包含了所有全局变量
  2. 函数执行时会创建一个“运行期上下文”的内部对象,它定义了函数执行的环境。每个运行期上下文都有自己的作用域链
  3. 函数[[Scope]]属性中的集合被复制到运行期上下文对象上,组成了一个新的对象,称为“活动对象”
  4. 函数执行完毕后,运行期上下文被销毁,活动对象也随之销毁

今天就到这吧了……不定期更新