JS - 深入理解 Javascript 原型和闭包

主要是一系列相关的概念介绍,需要有一定的相关的JS基础。

一、一切都是对象

一切都是对象 这句话的重点在于如何去理解“对象”这个概念。

——当然,也不是所有的都是对象,值类型就不是对象。

简单类型与复杂类型

JS分为简单类型和复杂类型,简单类型又叫做基本数据类型或者值类型,复杂类型又叫做引用类型

值类型:简单数据类型/基本数据类型,在存储时变量中存储的是值本身,因此叫做值类型。 ( Number, String, Boolean, Null、Undefined、Symbol)

引用类型:复杂数据类型,在存储时变量中存储的仅仅是地址(引用),因此叫做引用数据类型,通过 new 关键字创建的对象(系统对象、自定义对象),如 Object、Array、Date等。

所以这句话的理解应该是:一切(引用类型)都是对象,对象是属性的集合

二、函数和对象的关系

对象都是通过函数创建的

举个例子

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

虽然看起来跟函数没什么关系,但本质是依靠函数构造的。

//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;

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

三、prototype原型

之前第二点提到,对象都是通过函数创建的。但是第一点又说一切都是对象,包括函数也是对象。他也是属性的集合,可以对函数进行自定义属性。

这是因为函数的一个属性——prototype。每个函数都有一个属性叫做prototype

这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。

在这里插入图片描述
如上图,SuperType是是一个函数,右侧的方框就是它的原型。

原型既然作为对象,属性的集合,不可能就只弄个constructor来玩玩,肯定可以自定义的增加许多属性。例如这位Object大哥,人家的prototype里面,就有好几个其他属性。

在这里插入图片描述
也可以在自己自定义的方法的prototype中新增自己的属性

function Fn() { }
Fn.prototype.name = 'name';
Fn.prototype.getName = function () {
     return this.name;
 };

const fn = new Fn();
console.log(fn.name); // name
console.log(fn.getName()); // name

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

因为每个对象都有一个隐藏的属性——“proto”,这个属性引用了创建这个对象的函数的prototype。即:fn.proto === Fn.prototype

这里的"proto"成为“隐式原型”

四、隐式原型

每个函数function都有一个prototype,即原型。这里再加一句话——每个对象都有一个__proto__,指向创建该对象的函数的prototype

obj这个对象本质上是被Object函数创建的,因此obj.proto=== Object.prototype。我们可以用一个图来表示。

在这里插入图片描述
即,每个对象都有一个__proto__属性,指向创建该对象的函数的prototype。

那么上图中的“Object prototype”也是一个对象,它的__proto__指向哪里?

Object.prototype 是一个特例——它的__proto__指向的是null。

在这里插入图片描述

原型和原形链在这里讲的比较简单,可以去看之前一篇比较详细的文章 JS -原型与原形链

五、instanceof

对于值类型,你可以通过typeof判断,string/number/boolean都很清楚,但是typeof在判断到引用类型的时候,返回值只有object/function,你不知道它到底是一个object对象,还是数组。这个时候就需要用到instanceof。

function Foo() {};
var f1 = new Foo();

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

上图中,f1这个对象是被Foo创建,但是“f1 instanceof Object”为什么是true呢?

在这里插入图片描述

nstanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。

Instanceof的判断队则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。

所以,上面代码 f1 instanceof Object ”为 true。

六、原型链

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

function Foo() {};
var f1 = new Foo();

f1.a = 10;

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

console.log(f1.a) // 10
console.log(f1.b) // 100

以上代码中,f1是Foo函数new出来的对象,f1.a是f1对象的基本属性,f1.b是怎么来的呢?——从Foo.prototype得来,因为f1.__proto__指向的是Foo.prototype

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

七、执行上下文

浏览器在执行代码前需要一些“准备工作”:

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

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

如果在函数中,除了以上数据之外,还会有其他数据。

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

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

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

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

其实这是一个压栈出栈的过程——执行上下文栈

在这里插入图片描述

上下文栈的压栈、出栈过程:

  • 在执行代码之前,首先将创建全局上下文环境
  • 代码执行,上下文环境中的变量被赋值
  • 调用函数,执行函数体语句之前,会创建一个新的执行上下文环境,并将这个执行上下文环境压栈,设置为活动状态
  • 函数执行完毕后,调用函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)

八、作用域

作用域是一个很抽象的概念,类似于一个“地盘”。
在这里插入图片描述

如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

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

九、作用域 和 执行上下文

首先明确一下,作用域在函数定义时确定, 执行上下文是在函数调用时确定

在这里插入图片描述

作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。

同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。

所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

可以看之前比较详细的文章 JS - 作用域和执行上下文的区别

十、自由变量和作用域链

自由变量,就是在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的)。对于A作用域来说,x就是一个自由变量。

var x = 10;
function fn() {
    console.log(x + 20); // x 就是一个自由变量
}

那自由变量x 应该到哪个作用域取呢?

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

function show(f) {
    var x = 20;

    (function() {
        var x = 30
        f()
    })();
}
show(fn);

要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。

上面描述的只是跨一步作用域去寻找。

如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

这个一步一步“跨”的路线,我们称之为——作用域链。

十一、this

this的取值,分四种情况。

  1. 构造函数
function Foo() {
    this.name = "name";

    console.log(this);
}

var f1 = new Foo(); // Foo {name: 'name'}
console.log(f1.name); // name
  1. 函数作为对象的一个属性, this 由调用对象决定
var obj = {
    name: "name",
    fn: function () {
        console.log(this); // {name: 'name', fn: ƒ}
        console.log(this.name); // name
    }
}
obj.fn();
var name = "window";
var obj = {
    name: "name",
    fn: function () {
        console.log(this); // window
        console.log(this.name); // window
    }
}
obj.fn();
var foo = obj.fn;
foo()
  1. 函数用call或者apply调用,this的值就取传入的对象的值。
var x = 10;
var obj = {
    x: 20
};

var fn = function() {
    console.log(this.x);
}
fn(); // 10
fn.call(obj) //20
  1. 全局 & 调用普通函数
var x = 10;
var fn = function() {
    console.log(this.x);
}
fn(); // 10

this 的调用还要区分 普通函数和普通箭头函数 JS-普通函数和箭头函数的this区别

十二、闭包

你只需要知道应用的两种情况即可——函数作为返回值,函数作为参数传递。

  1. 函数作为返回值
function fn() {
    var max = 10;
    return function bar(x) {
        if(x>max) {
            console.log(x);
        }
    }
}

var f1 = fn();
f1(15) 
// 15

如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。

  1. 函数作为参数被传递
var max = 10;
var fn = function (x) {
    if(x>max) {
        console.log(x);
    }
};
(function (f) {
    var max = 100;
    f(15);
})(fn); 
// 15

fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。


深入理解javascript原型和闭包

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值