转载

深入理解JavaScript原型和闭包

本文是本人阅读学习王福朋的博客 - 深入理解JavaScript原型和闭包 时所记的笔记。

1、一切都是对象

先说结论,一切引用类型都是对象,对象是属性的集合。

首先我们对不同变量使用 typeof() 看看都有哪些输出的类型。

console.log(typeof(x));                 // undefined
console.log(typeof(10));                // number
console.log(typeof('abc'));             // string
console.log(typeof(true));              // boolean
console.log(typeof(function () {}));    //function
console.log(typeof([1, 'a', true]));    //object
console.log(typeof ({ a: 10, b: 20 })); //object
console.log(typeof (null));             //object
console.log(typeof (new Number(10)));   //object

在以上代码中, undefined , number , string , boolean 属于 值类型 ,不是对象。而其他的几种类型 - 包括函数、数组、对象、 nullnew Number(10) 都是对象,它们属于 引用类型

在JavaScript中,数组是对象,函数是对象,对象还是对象。对象里面的一切都是属性,只有属性,没有方法,或者说方法也是一种属性。属性表示为 键值对 的形式。

JavaScript中的对象可以任意的扩展属性,定义属性的方法通常有两种。

var obj = {
  a = 10,
  b: function(x) {
    console.log(this.a + x)
  },
  c: {
    name: "Steven",
    year: 1988
  }
}

上面这段代码中, obj 是一个自定义的对象,其中 abc 是它的属性,而属性 c 的本身又是一个对象,它又有 nameyear 两个属性。

函数和数组不能用上面的方法定义属性,下面以函数为例:

var fn = function () {
  alert(100);
};
fn.a = 10;
fn.b = function () {
  alert(123);
};
fn.c = {
  name: "Steven",
  year: 1988
};

在jQuery源码中,变量 jQuery 或者 $ 其实是一个函数,我们可以用 typeof() 验证一下:

console.log(typeof ($));  // function
console.log($.trim(" ABC "));

很明显,这就是在 $ 或者 jQuery 函数上加了一个 trim 属性,属性值是函数,作用是截取前后空格。

2、函数和对象的关系

上文已经说到,函数也是一种对象,我们可以用 instanceof 验证一下:

var fn = function () { };
console.log(fn instanceof Object);  // true

但是函数和对象的关系却有一点复杂,请看下面这个例子:

function Fn() {
  this.name = '严新晨';
  this.year = 1990;
}
var fn_1 = new Fn();

由上面这个例子可以得出,对象是可以通过函数创建的。

但其实, 对象都是通过函数创建的

var obj = { a: 10, b: 20 };
var arr = [5, 'x', true];

上面这种方式,其实是一个语法糖,而这段代码的本质是:

var obj = new Object();
obj.a = 10;
obj.b = 20;

var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;

而其中的 ObjectArray 都是函数:

console.log(typeof (Object));  // function
console.log(typeof (Array));  // function

由此可以得出, 对象都是通过函数创建的

3、prototype原型

每个函数都有一个默认属性 - prototype

这个 prototype 的属性值是一个对象,这个对象有一个默认属性 - constructor ,这个属性指向这个函数本身。

深入理解JavaScript原型和闭包

而原型作为一个对象,除了 constructor 之外,当然可以有其他属性,以函数 Object 为例,在浏览器调试窗口输入 Object.prototype 会得到以下返回值:

Object
  ...
  constructor
  hasOwnProperty
  isPrototypeOfs
  toLocalString
  toString
  valueOf
  ...

同时,我们还可以为这个原型增添自定义方法或属性

function Fn(){}
Fn.prototype.name = "Steven"
Fn.prototype.getYear = function(){
  return 1988;
}

var fn = new Fn();
console.log(fn.name);
console.log(fn.getYear());

在上例中, Fn 是一个函数, fn 对象是从 Fn 函数中 new 出来的,这样 fn 对象就可以调用 Fn.prototype 中的属性。

每个对象都有一个隐藏的属性 - __proto__ ,这个属性引用了创建这个对象的函数的 prototype 。即: fn.__proto__ === Fn.prototype

这里的 __proto__ 称为“隐式原型”。

4、隐式原型

每个函数function都有一个 prototype ,即原型。

每个对象都有一个 __proto__ ,可称为隐式原型。

var obj = {}
console.log(obj.__proto__ === Object.prototype) // true

每个对象都有一个 __proto__ 属性,指向创建该对象的函数的 prototype

function Foo(){}
Foo.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true 
Function.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype // true

注意, Object.prototype.__proto__ 是一个特例,它指向的是 null

5、instanceof

由于 typeof 在判断引用类型时,返回值只有 objectfunction ,这时我们可以用到 instanceof

function Foo(){}
var f = new Foo()

console.log(f instanceof Foo) // true
console.log(f instanceof Object) // true

用法: A instanceof B ,变量 A 是一个待判断的对象,变量 B 通常是一个函数。

判断规则:沿着 A.__proto__B.prototype 查找,如果能找到同一个引用,即同一个对象,则返回 true

由以上判定规则,我们可以解释许多奇怪的判定结果,例如:

Object instanceof Function // true
Function instanceof Object // true
Function instanceof Function // true

深入理解JavaScript原型和闭包

instanceof 表示的是一种继承关系 - 原型链

6、继承

JavaScript中的继承通过原型链来体现。

function Foo(){}
var f = new Foo()

f.a = 10

Foo.prototype.a = 100
Foo.prototype.b = 200

console.log(f.a) // 10
console.log(f.b) // 200

上例中,f是Foo函数new出来的对象,f.a是对象f的基本属性,因为 f.__proto__ === Foo.prototype ,所以f.b是从 Foo.prototype 中继承而来的。

在JavaScript中,访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着 __proto__ 这条链向上找,这就是原型链

通过 hasOwnProperty ,我们可以判断出一个属性到底是基本属性,还是从原型中继承而来的。

function Foo(){}
var f_1 = new Foo()

f.a = 10

Foo.prototype.a = 100
Foo.prototype.b = 200

var item

for(item in f){
  console.log(item) // a b
}

for(item in f){
  if(f.hasOwnProperty(item){
    console.log(item) // a
  })
}

hasOwnProperty 方法是从 Object.prototype 中继承而来的

每个函数都有 applycall 方法,都有 lengtharguments 等属性,这些都是从 Function.prototype 中继承而来的

由于 Function.prototype.__proto__ 指向 Object.prototype ,所以函数也会有 hasOwnProperty 方法

7、原型的灵活性

首先,对象属性可以随时改动

其次,如果继承的方法不合适,可以随时修改

var obj = { a: 10, b: 20 }
console.log(obj.toString()) // [object Object]

var arr = [1, 2, true]
console.log(arr.toString()) // 1, 2, true

从上例中可以看出, ObjectArraytoString() 方法是不一样的,肯定是 Array.prototype.toString() 作了修改。

同理,我们也可以自己定义一个函数并修改 toString() 方法。

function Foo(){}
var f = new Foo()

Foo.prototype.toString = function(){
  return "严新晨"
}

console.log(f.toString) // 严新晨

最后,如果缺少需要的方法,也可以自己创建

如果要添加内置方法的原型属性,最好做一步判断,如果该属性不存在,则添加。如果本来就存在,就没必要再添加了。

8、简述 - 执行上下文 - 上

执行上下文,也叫执行上下文环境

console.log(a) // 报错,a is not undefined
console.log(a) // undefined
var a;
console.log(a) // undefined
var a  = 10;
console.log(this) // Window {...}
console.log(f_1) // function f_1({})
function f_1(){} // 函数声明

console.log(f_2) // undefined
var f_2 = function(){} // 函数表达式

在js代码执行前,浏览器会先进行一些准备工作:

  • 变量、函数表达式 - 变量声明,默认赋值为 undefined

  • this - 赋值;

  • 函数声明 - 赋值;

这三种数据的准备工作我们称之为“执行上下文”或者“执行上下文环境”。

JavaScript在执行一个代码段之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况 - 全局代码函数体eval代码

首先,全局代码,写在 <script> 标签里的代码段

其次,eval接收的是一段文本形式的代码(不推荐使用)

最后,函数体代码段是函数在创建时,本质上是 new Function(…) 得来的,其中需要传入一个文本形式的参数作为函数体

var fn = new Function("x", "console.log(x+5)")

9、简述 - 执行上下文 - 下

function fn(x){
  console.log(arguments)
  console.log(x)
}
fn(10)

以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出, 函数每被调用一次,都会产生一个新的执行上下文环境 。因为不同的调用可能就会有不同的参数。

函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。

var a = 10
function fn(){
  console.log(a)
}

function bar(fn){
  var a = 20
  fn() // 10
}
bar(fn)

总结一下上下文环境的数据内容

  • 普通变量 - 声明

  • 函数表达式 - 声明

  • 函数声明 - 赋值

  • this - 赋值

    如果代码段是函数体,则需加上

  • 参数 - 赋值

  • arguments - 赋值

  • 自由变量的取值作用域 - 赋值

所以通俗来讲,执行上下文环境就是在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空

10、this

this的取值,通常分4种情况

先强调一点, 在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了 ,因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

情况1:构造函数

所谓构造函数就是用来new对象的函数。

注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。

function Foo(){
    this.name = "严新晨"
    this.year = 1990

    console.log(this) // Foo {name: "严新晨", year: 1990}
}

var f = new Foo();

console.log(f.name) // 严新晨
console.log(f.year) // 1990

如果函数作为构造函数调用,那么其中的this就代表它即将new出来的对象。

如果直接调用Foo函数,而不是new Foo(),情况就完全不同。

function Foo(){
    this.name = "严新晨"
    this.year = 1990

    console.log(this) // Window {...}
}
Foo()

情况2:函数作为对象的一个属性

如果函数作为对象的一个属性,并且作为对象的一个属性被调用时,函数中的this指向该对象。

var obj = {
    x: 10,
    fn: function(){
        console.log(this) // Object {x: 10, fn: function}
        console.log(this.x) // 10
    }
}
obj.fn()

如果函数fn是对象obj的一个属性,但是不作为obj的一个属性被调用

var obj = {
    x: 10,
    fn: function(){
        console.log(this) // Window {...}
        console.log(this.x) // undefined
    }
}
var f = obj.fn
f()

情况3:函数用call或者apply调用

当一个函数被call和apply调用时,this的值就取传入的对象的值。

var obj = { x: 10 }
var fn = function(){
    console.log(this) // Object {x:10}
    console.log(this.x) // 10
}
fn.call(obj)

情况4:全局&调用普通函数

在全局环境下,this永远是Window

普通函数在调用时,其中的this也都是Window

var x = 10
var fn = function(){
    console.log(this) // Window
    console.log(this.x) // 10
}
fn()

下面情况需要注意一下

var obj = {
  x: 10,
  fn: function(){
    function foo(){
      console.log(this) // Window
      console.log(this.x) // undefined
    }
    foo()
  }
}
obj.fn()

函数 foo 虽然是在 obj.fn 内部定义的,但是它仍然是一个普通的函数, this 仍然指向 window

11、执行上下文栈

上文说过,执行全局代码时,会产生一个全局上下文环境,每次调用函数都又会产生函数上下文环境。当函数调用完成时,函数上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。 处于活动状态的执行上下文环境始终只有一个。其实这是一个压栈出栈的过程 - 执行上下文栈。

深入理解JavaScript原型和闭包

以下面代码为例:

var a = 10, // 1、进入全局上下文环境
    fn,
    bar = function(x){
      var x = 5
      fn(x+b) // 3、进入fn()函数上下文环境
    }

fn = function(y){
  var c = 5
  console.log(y+c)
}

bar() // 2、进入bar()函数上下文环境

执行代码前,首次创建全局上下文环境

a === undefined
fn === undefined
bar === undefined
this === window

代码执行时,全局上下文环境中的各个变量被赋值

a === 10
fn === function
bar === function
this === window

调用 bar() 函数时,会创建一个新的函数上下文环境

b === undefined
x === 5
arguments === [5]
this === window

深入理解JavaScript原型和闭包

深入理解JavaScript原型和闭包

深入理解JavaScript原型和闭包

深入理解JavaScript原型和闭包

以上是一段简短代码的执行上下文环境的变化过程,一个完整的闭环。

但实际上,上述情况是一种理想的情况。而有一种很常见的情况,无法做到这样干净利落的说销毁就销毁,那就是闭包。

12、简述 - 作用域

JavaScript没有块级作用域。所谓的“块”就是“{}”中的语句,比如: if(){} 或者 for(){} 之类的。

所以,编写代码时不要在“块”里声明变量。

重点来了: JavaScript除了全局作用域之外,只有函数可以创建的作用域

所以,在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。

作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

13、作用域和上下文环境

在上文中已经说过,除了全局作用域之外,每个函数都会创建自己的作用域。 作用域在函数定义时就已经确定了,而不是在函数调用时确定

var a = 10,
    b = 20;

function fn(x){
  var a = 100,
      c = 300;
  
  function bar(x){
    var a = 1000,
        d = 4000;
  }
  bar(100);
  bar(200);
}
fn(10)

未完待续

原文  https://segmentfault.com/a/1190000008215428
正文到此结束
Loading...