资讯专栏INFORMATION COLUMN

深入理解JavaScript原型和闭包

missonce / 913人阅读

摘要:本文是本人阅读学习深入理解原型和闭包时所作的总结和笔记,当然也引用了很多原文,感兴趣的朋友也可以直接去看原文。即这里的称为隐式原型。注意,构造函数的函数名第一个字母大写规则约定。但实际上,上述情况是一种理想的情况。

本文是本人阅读学习深入理解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,这个属性指向这个函数本身。

而原型作为一个对象,除了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

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代码

首先,全局代码,写在

以下面代码为例:

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

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

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

12、简述 - 作用域

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

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

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

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

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

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

13、作用域和上下文环境

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

var a = 10,
    b = 20;
    // 全局作用域:a=10,b=20

function fn(x){
  var a = 100,
      c = 300;
      // fn(10):a=100,c=300,x=10

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

作用域只是一个“地盘”,一个抽象的概念,其中没有变量
要通过作用域对应的执行上下文环境来获取变量的值
**同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
所以,作用域中变量的值是在执行过程中产生的确定的,而作用域是在函数创建时就确定了
所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值

14、从自由变量到作用域链

上文中有一种常见情况我们并没有讨论,那就是跨作用域取值的情况。

先说明一个概念 - 自由变量

var a = 10
function fn(){
  var b = 20
  return a + b // 这里的a就是一个自由变量
}

很多人对此解释为a是从父作用域取值的,这种说法基本正确,但有些时候会产生歧义。

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

function show(f){
  var x = 20;
  (function(){
    f() // 10,而不是20
  })()
}

show(fn);

所以,更准确的说法是,我们要到创建这个函数的作用域中去取值

var a = 10;
function fn() {
  var b = 20;
  function bar() {
    console.log(a + b);
    // 创建函数bar()时,b=20
    // 通过作用域链查找到a=10
  }
  return bar;
}

var x = fn(),
    b = 200;

x(); // 30
15、闭包

先回顾下前面章节讲到的两个重点:

自由变量跨作用域取值时,要去创建函数的作用域取值,而不是调用函数的作用域;

当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。

“闭包”这个词的概念很不好解释,但我们只需记住两种情况即可:函数作为返回值和函数作为参数传递

首先,函数作为返回值,先看个例子

// 1、全局作用域,max=100,其他变量undefined
function fn() {
  // 2、fn()作用域,max=10(调用结束后销毁),其他变量undefined
  var max = 10;
  return function bar(x) {
    // 3、bar()作用域,max=10,x=15(调用结束后销毁)
    if (x > max) {
      console.log(x);
    }
  }
}
var f1 = fn(), // bar()作为返回值赋值给f1
    max = 100;
f1(15); // 15

然后,函数作为参数传递,再看个例子

// 全局作用域,max=10
var max = 10,
    fn = function (x) {
      if (x > max) {
        console.log(x) // 15
      }
    };
(function (f) {
  var max = 100;
  f(15); // max=10而不是100
})(fn);

先回顾下前面章节讲到的两个重点:

自由变量跨作用域取值时,要去创建函数的作用域取值,而不是调用函数的作用域;

当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。

16、完结 - 这章没干货我也就不写了o(╯□╰)o 17、补充 - this - 我直接写在10、this那一篇里了就不赘述了 18、补充 - 上下文环境和作用域的关系

本篇主要是解释一下上下文环境和作用域并不是一回事

上下文环境 - 可以理解为一个看不见摸不着的对象(有若干个属性),在调用函数时创建,用来保存调用函数时的各个变量。

作用域 - 除了全局作用域,只有创建函数才会创建作用域,无论你是否调用函数,函数只要创建了就有一个独立的作用域。

两者 - 一个作用域可能包含若干个上下文环境,也可能从来没有过上下文环境(函数从未被调用),还可能函数调用完毕后上下文环境被销毁了等多种情况。

以下面代码为例:

// 全局作用域中x=100
var x = 100;
function fn(x) {
  // fn(x)作用域
  // 调用f1()时的上下文环境中,x=5
  // 调用f2()时的上下文环境中,x=10
  return function () {
    // 匿名function作用域
    console.log(x);
  }
}
var f1 = fn(5),
    f2 = fn(10);
f1(); // 5
f2(); // 10

所谓上下文环境就是调用函数时创建的一个临时作用域,根据调用情况不同里面的变量会发生变化;而作用域是随着函数的创建而创建,里面的变量可能会在调用时被上下文环境中相同变量覆盖。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81366.html

相关文章

  • JavaScript深入浅出

    摘要:理解的函数基础要搞好深入浅出原型使用原型模型,虽然这经常被当作缺点提及,但是只要善于运用,其实基于原型的继承模型比传统的类继承还要强大。中文指南基本操作指南二继续熟悉的几对方法,包括,,。商业转载请联系作者获得授权,非商业转载请注明出处。 怎样使用 this 因为本人属于伪前端,因此文中只看懂了 8 成左右,希望能够给大家带来帮助....(据说是阿里的前端妹子写的) this 的值到底...

    blair 评论0 收藏0
  • 深入理解javascript原型闭包

    摘要:深入理解原型和闭包王福朋博客园深入理解原型和闭包一切都是对象原文链接本文要点一切引用类型都是对象,对象是属性的集合。每个对象都有一个,可称为隐式原型。另外注意,构造函数的函数名第一个字母大写规则约定。 深入理解javascript原型和闭包 王福朋 - 博客园 —— 《 深入理解javascript原型和闭包》 1. 一切都是对象 原文链接:http://www.cnblogs.com...

    jemygraw 评论0 收藏0
  • 深入理解JavaScript,这一篇就够了

    摘要:也就是说,所有的函数和构造函数都是由生成,包括本身。如果只考虑构造函数和及其关联的原型对象,在不解决悬念的情况下,图形是这样的可以看到,每一个构造函数和它关联的原型对象构成一个环,而且每一个构造函数的属性无所指。 前言  JavaScript 是我接触到的第二门编程语言,第一门是 C 语言。然后才是 C++、Java 还有其它一些什么。所以我对 JavaScript 是非常有感情的,毕...

    villainhr 评论0 收藏0
  • 深入理解javascript原型闭包

    摘要:情况构造函数所谓构造函数就是用来对象的函数。另外注意,构造函数的函数名第一个字母大写规则约定。闭包但是你只需要知道应用的两种情况即可函数作为返回值,函数作为参数传递。如上代码,函数作为返回值,赋值给变量。这就是需要理解闭包的核心内容。 原文链接http://www.cnblogs.com/wangfupeng1988/p/3977924.html 对象是属性的集合。 function ...

    _ang 评论0 收藏0
  • JS笔记

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。异步编程入门的全称是前端经典面试题从输入到页面加载发生了什么这是一篇开发的科普类文章,涉及到优化等多个方面。 TypeScript 入门教程 从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。 网络基础知识之 HTTP 协议 详细介绍 HTT...

    rottengeek 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<