深入理解JavaScript原型与闭包

Posted BY彡阿长

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解JavaScript原型与闭包相关的知识,希望对你有一定的参考价值。

说明

本文为作者学习记录相关笔记及理解,如有不妥之处,请各位读者积极指出,
虽然标题是深入理解,但可能存在许多不够深入的地方,请各位小伙伴不吝赐教

一切都是对象

一切引用类型都是对象,对象是属性的集合

值类型就不是对象

函数和对象的关系

对象都是通过函数创建的

对象是若干属性的集合,一切引用类型都是对象

var obj = {name: 'zs', age: 20};
//等价于
var obj = new Object();
obj.name = 'zs';
obj.age = 20;

每个函数都有一个属性prototype,其属性值是一个对象,默认只有一个叫constructor的属性,指向这个函数本身

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

Object.prototype.__proto__ ===null

函数也是对象,也有__proto__ Object.__proto__===Function.prototype

Function也是一个函数,是一种对象,有__proto__ 属性,它一定是被Function创建,所以Function是被自身创建,它的__proto__指向自身的prototype

Function.prototype.__proto__===Object.prototype

Function.prototype所指向的对象也是被Object创建的对象

原型对象和对象的关系

javascript 中,对象由一组或多组的属性和值组成:

{
  key1: value1,
  key2: value2,
  key3: value3,
}

不管是对象,还是函数和数组,它们都是Object的实例,也就是说在 JavaScript 中,除了原始类型以外,其余都是对象

函数和对象的关系

函数是一种特殊的对象

对象都是通过函数创建的

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

原型prototype

默认情况下,每个函数都有一个默认的属性prototype,是一个对象,称为原型对象,默认只有一个constructor属性,指向这个函数本身

function Person(name) {
  this.name = name;
}
console.log(Person.prototype.constructor === Person);
//true

隐式原型__proto__

每个对象都有一个隐式原型__proto__,是一个隐藏属性,指向创建该对象的函数的prototype

var obj = {
    name: "张三",
    age: 20
};

可以看出,我们平时用定义的普通对象,就是用Object这个构造函数创建的.

既然Object是一个函数,那么它也就有prototype属性了

那么Object又是被谁创建出来的呢?

虽然Object是一个函数,但是函数也是对象,那么就有__proto__属性,我们通过这个隐式原型就可以知道它是谁创建出来的了

⚠️我们直接在控制台输入Object.__proto__是看不到想要的结果的

只能得到下图,说明真的是由一个函数创建的

var obj = {name: "张三"};
console.log(obj.__proto__);

我们先获取一个普通对象的隐式原型,找到Object函数,然后查看Object对象的隐式原型,即找到了创建了Object的函数,也就是Function函数

函数的原型对象也是一个对象,也拥有自己的原型对象

函数是Object的实例,因此函数的原型对象为Object的原型对象

function Person(name) {
  this.name = name;
}
console.log(Person.prototype.__proto__ === Object.prototype)
//true

特例

Object.prototype__proto__指向null

因此obj.__proto__Object.prototype的属性相等

通俗来说就是实例对象的原型对象等于其构造函数的原型对象.

小结
  • 每个函数的原型对象(Person.prototype)都拥有constructor属性,指向该函数本身

  • 使用构造函数(new Person())可以创建对象,创建的对象称为实例对象(zs);

  • 实例对象通过将__proto__属性指向构造函数的原型对象(Person.prototype),实现了该原型对象的继承。

JS数据类型

  • 原始数据类型 存储在栈上
    • boolean
    • number
    • string
    • null
    • undefined
    • symbol(es6)
    • bigint(es10)

bigint它提供了一种方法来表示大于 253 - 1 的整数

  • 对象类型 存储在堆上,引用地址还是存在栈上
    • Obeject
    • Function

类型判断

  • 值类型用typeof
  • 引用类型用instansof
function show(x) {

  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
}
show();

instanceof

类型判断

  • 基本数据类型用typeof判断即可
  • 引用类型用instansof来进行判断

判断规则

typeof在判断引用类型时,只会返回ObjectFunction

instanceof运算符却可以判断引用类型,这是什么原理呢?

请看如下规则↓

A instanceof B

第一个参数是一个对象A,第二个对象是一个函数B

规则:沿着A的__proto__这条线来查找,同时沿着B的prototype这条线来查找,当两条线查找到同一个引用时,说明为同一对象,返回true,如果到终点还没找到,返回false

instanceof代表一种继承关系,或者原型链的结构

JavaScript继承通过原型链体现:访问一个对象的属性时,先在对象自身的属性中查找,如果没有找到再沿着__proto__查找其构造函数上是否有该属性.

hasOwnproperty()判断是否是自身的属性,从Object.prototype中来

所有对象的原型链都会找到Object.prototype

继承

在访问一个对象的属性和方法时,先在其自身的属性和方法中查找,如果没有找到,再到其__proto__中查找,依次类推,直到null

我们都知道toString()这个方法,所有实例化的对象基本都有这个方法,我们创建时也没有定义,那么它是哪里来的呢?答案是继承来的

例如

var obj = {name: "张三"};

obj.toString === Object.prototype.toString;
//true

原型链

__proto__prototype的关系

  • 每个对象都有__proto__属性来标识自己所继承的原型对象,但只有函数才有prototype属性
  • 每个函数都有一个prototype属性,该属性为该函数的原型对象
  • 通过将实例对象的__proto__属性赋值给其构造函数的原型对象prototypeJavaScript可以使用构造函数来创建对象的方式,实现继承

一个对象可通过__proto__访问原型对象上的属性和方法,而该原型同样也可通过__proto__访问它的原型对象,这样我们就在实例和原型之间构造了一条原型链

执行上下文

在执行JavaScript代码之前,需要一系列的准备工作,其中比较重要的就是编译阶段,而编译阶段主要负责执行上下文的创建

执行上下文也称为词法环境

JavaScript中有三种执行上下文

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval

注意:eval很少用,且极不推荐使用

在创建上下文的过程中,主要包括三步

  • 建立作用域链(Scope Chain);
  • 创建变量对象(Variable Object,简称 VO);
  • 确定 this的指向。

本文主要介绍创建变量对象

变量对象和执行上下文直接关联,在这个上下文中的所有变量和函数都是在这个对象之上创建的。

例如:全局环境中的window,我们使用var关键字创建的变量都是window对象的一个属性,这一点可自行验证

创建变量对象的过程

  • 变量声明:分配内存,初始化为undefined
  • 函数声明:内存中创建函数对象,并初始化为函数对象

如果是函数执行上下文

  • 初始化参数
  • 创建arguments对象
  • 确定自由变量的取值作用域

自由变量:在函数中使用但未在函数中定义的变量

执行上下文栈

第一次加载JavaScript代码时,会创建一个全局执行上下文,每次调用函数又会产生函数执行上下文并将其设置为当前活动上下文,函数执行完成会退出,销毁自身的执行上下文,又回到全局上下文

这样的过程就是执行上下文栈的压栈出栈过程

作用域

作用域可以理解为当前执行上下文,注意是当前

作用域本身没有变量和方法的值,只有在对应的执行上下文中才有

也就是说处于不同执行上下文的变量会有不同的取值

例如

var age = 10;
function test () {
    var age = 100;
    console.log(age);
};
test();//100

以上代码输出100,因为在test函数的执行上下文中,变量age的值为100,而变量age的作用域是在函数调用时就确定了,所以虽然我们是在全局作用域下调用的函数,但age的取值还是会回到定义它的那个作用域去寻找

作用域也是有上下级关系的,确定了函数或变量是在哪个作用域下创建的

例如:

function test () {
    var a = 100;
};
console.dir(test);

可以看到当前test函数处于Global作用域

总结

作用域只是一个空间,里面没有变量,需要通过对应的当前执行上下文来获取变量,注意是当前执行上下文,因为在创建阶段只是声明,在执行阶段才会进行赋值

同一个作用域下,不同的调用会产生不同的当前执行上下文,继而产生不同的变量值,也就是说变量值是在执行过程中确定下来的

要查找一个作用域下的某个变量值,就需要找到这个作用域对应的当前执行上下文

闭包

闭包简单来说就是在一个函数内部使用外部函数的值

一般有两种情况

  • 函数作为返回值传递
  • 函数作为参数传递

这两种都称为高阶函数

函数作为返回值传递

示例:

function fn() {
	var age = 10;    
	return function bar(num) {
		console.log("bar函数",num + age);
    }
};
var f1 = fn();
f1(15);//bar函数 25

bar函数作为fn函数的返回值,赋值给f1变量,执行f1(15)时,用到了fn作用域下的age变量

通过调试可以看到,fn函数就是一个闭包,Closure标识

可以简单理解为如果函数的返回值是一个函数,并且返回的函数中用到了外层函数的变量,那么外层函数就是一个闭包

示例:如果没有使用变量,也就不存在闭包

function fn() {
	var age = 10;    
	return function bar(num) {
		console.log("bar函数",num);
    }
};
var f1 = fn();
f1(15);//bar函数 15

函数作为参数传递

var age = 10;
var fn = function(num) {
    console.log(num + age)
};
(function f(f) {
    var age = 100;
    f(15);
})(fn);
//25

fn函数作为一个参数传进另外一个函数,本例中是一个立即执行函数,fn函数赋值给f参数,执行f(15)时,age变量的取值是10,

因为上文说过,要去创建函数的作用域中取值,函数fn在全局作用域取值,全局作用域中age的值为10

顺便说说,以下内容为扩展

通过原型链访问对象的方法和属性

JavaScript 中,是通过遍历原型链的方式,来访问对象的方法和属性。

  • JavaScript 试图访问一个对象的属性时,会基于原型链进行查找。查找的过程是这样的:

  • 首先会优先在该对象上搜寻。如果找不到,还会依次层层向上搜索该对象的原型对象、该对象的原型对象的原型对象等(套娃告警);

  • JavaScript 中的所有对象都来自Object,Object.prototype.__proto__ === nullnull没有原型,并作为这个原型链中的最后一个环节;

  • JavaScript 会遍历访问对象的整个原型链,如果最终依然找不到,此时会认为该对象的属性值为undefined

由于通过原型链进行属性的查找,需要层层遍历各个原型对象,此时可能会带来性能问题:

  • 当试图访问不存在的属性时,会遍历整个原型链;
  • 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。

因此,我们在设计对象的时候,需要注意代码中原型链的长度。当原型链过长时,可以选择进行分解,来避免可能带来的性能问题。

使用 prototype 和 proto 实现继承

JavaScript对象的属性值可以为任意类型。因此,属性的值同样可以为另外一个对象,这意味着 JavaScript 可以这么做:通过将对象 A 的__proto__属性赋值为对象 B,即A.__proto__ = B,此时使用A.__proto__便可以访问 B 的属性和方法。

var zs = new Person("张三");

JavaScript引擎实际上执行了以下代码

var zs = {};
zs.__proto__ = Person.prototype;
Person.call(zs, '张三');

以上是关于深入理解JavaScript原型与闭包的主要内容,如果未能解决你的问题,请参考以下文章

深入理解javascript原型和闭包(12)——闭包

深入理解javascript原型和闭包(完结)

深入理解javascript原型和闭包(12)——简介作用域

深入理解javascript原型和闭包——函数和对象的关系

深入理解javascript原型和闭包(完结)

深入理解javascript原型和闭包——函数和对象的关系 (转载)