深入理解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
在判断引用类型时,只会返回Object
和Function
而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__
属性赋值给其构造函数的原型对象prototype
,JavaScript
可以使用构造函数来创建对象的方式,实现继承
一个对象可通过__proto__
访问原型对象上的属性和方法,而该原型同样也可通过__proto__
访问它的原型对象,这样我们就在实例和原型之间构造了一条原型链
执行上下文
在执行JavaScript
代码之前,需要一系列的准备工作,其中比较重要的就是编译阶段,而编译阶段主要负责执行上下文的创建
执行上下文也称为词法环境
JavaScript
中有三种执行上下文
- 全局执行上下文
- 函数执行上下文
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__
===null
。null
没有原型,并作为这个原型链中的最后一个环节; -
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原型与闭包的主要内容,如果未能解决你的问题,请参考以下文章