原型与原型链

  1. 函数的prototype属性
  • 每个函数都有prototype属性,它默认指向一个Object空对象(即称为:原型对象)。
  • 原型对象中有一个属性constructor,它指向函数对象。
  1. 给原型对象添加属性(一般都是方法)
  • 原型中的方法都是给实例对象使用的
  • 作用:函数的所有实例对象自动拥有原型中的属性(方法)。

示例:

  1. <script>
  2. console.log(Date.prototype,typeof Date.prototype);
  3. //创建一个函数
  4. function Fun(){
  5. }
  6. console.log(Fun.prototype);
  7. //原型中的constructor属性指向函数对象
  8. console.log(Date.prototype.constructor === Date); //true
  9. console.log(Fun.prototype.constructor === Fun); //true
  10. Fun.prototype.test = function(){
  11. console.log('test()');
  12. }
  13. var fun = new Fun();
  14. //调用原型中的方法
  15. fun.test();
  16. </script>

显式原型和隐式原型

概念:

  1. 每个函数function都有一个prototype,即显式原型(属性)。
  2. 每个实例对象都有一个proto,即隐式原型(属性)。
  3. 实例对象的隐式原型的值等同于其对应构造函数显式原型的值。

示例:

  1. <script>
  2. function Fun() { };
  3. // 1. 每个函数function都有一个prototype,即显式原型(属性)。
  4. console.log(Fun.prototype);
  5. // 2. 每个实例对象都有一个__proto__,即隐式原型(属性)。
  6. var fun = new Fun();
  7. console.log(fun.__proto__);
  8. // 3. 实例对象的隐式原型的值等同于其对应构造函数显式原型的值。
  9. console.log(Fun.prototype === fun.__proto__);
  10. </script>

那么函数的prototype属性是怎么来的呢?我们声明函数的时候没有定义过呀。所以肯定是引擎加的,那什么时候加的呢,函数对象一创建,引擎就会自动帮我们加上prototype属性了,只不过指向一个空对象,相当于函数内部默认执行了this.prototype = {};
那么实例对象的proto属性是怎么来的呢?肯定也是引擎帮我们加的,而且是一创建实例化对象的时候就加上了,并且实例对象的proto属性和构造函数prototype是一样的,相当于执行了一条内部语句:this.__proto__ = Fun.prototype;
依据以上的知识我们可以画出大概的内存图:
image.png

总结:

  • 函数的prototype属性:在定义函数时自动添加,默认值是空Object对象。
  • 对象的proto属性:创建对象时自动添加,默认值为对应构造函数的prototype属性值。
  • 程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)。



原型链

概述

  • 访问一个对象的属性时:
    • 先在自身属性中查找,找到返回。
    • 如果没有,再沿着proto这条链向上查找,找到返回。
    • 如果最终没找到,返回undefined。
  • 原型链别名:隐式原型链
  • 作用:查找对象的属性(方法)
  • Object原型对象是原型对象的终点,任何函数都可以顺着原型链往上查找到Object原型对象,所以任何函数可以使用Object原型对象中定义的方法。

image.png
图解


构造函数/原型/实例对象的关系

任何对象的隐式原型等于Object的显式原型
image.png
任何函数的隐式原型等于Function的显式原型
image.png

补充

  1. 函数的显示原型指向的对象:默认是空Object对象(但Object不满足)

    1. <script>
    2. function Fn(){}
    3. console.log(Fn.prototype instanceof Object) // true
    4. console.log(Object.prototype instanceof Object) // false
    5. console.log(Function.prototype instanceof Object) // true
    6. </script>
  2. 所有函数都是Function的实例(包括Function本身)

    1. console.log(Function.prototype === Function.__proto__) // true
  3. Object的原型对象是原型链的尽头

    1. console.log(Object.prototype.__proto__) //null

属性问题

  1. 读取对象的属性值时:会自动到原型链中查找
  2. 设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加次属性并设置其值
  3. 方法一般定义再原型中,属性一般通过构造函数定义在对象本身上

示例

  1. <script>
  2. function Fn(){}
  3. Fn.prototype.a = 'xxx'
  4. var fn1 = new Fn()
  5. console.log(fn1.a)
  6. var fn2 = new Fn()
  7. fn2.a = 'yyy' //这里修改的只是对象自身的属性,并不会修改原型中的a属性
  8. console.log(fn1.a,fn2.a)
  9. function Person(name, age) {
  10. this.name = name
  11. this.age = age
  12. }
  13. Person.prototype.setName = function(name){
  14. this.name = name
  15. }
  16. var p1 = new Person('张三',50)
  17. p1.setName('李四 ')
  18. console.log(p1)
  19. var p2 = new Person('王五',17)
  20. console.log(p1.__proto__ === p2.__proto__)
  21. </script>

探索instanceof

  1. instanceof是如何判断的?
  • 表达式:A instanceof B (A是实例对象,B是构造函数对象)
  • 如果B函数的显示原型对象在A对象的原型链上,返回true,否则返回false

案例1

  1. <script>
  2. function Fn(){}
  3. var fn1 = new Fn()
  4. console.log(fn1 instanceof Fn) //true
  5. console.log(fn1 instanceof Object) //true
  6. </script>

image.png
图示
案例2

  1. <script>
  2. console.log(Object instanceof Function) //true
  3. console.log(Object instanceof Object) //true
  4. console.log(Function instanceof Function) //true
  5. console.log(Function instanceof Object) //true
  6. function Foo(){}
  7. console.log(Object instanceof Foo) //false
  8. </script>

此处不在画图示,其实”构造函数/原型/实例对象的关系”中的第二副图,基本以及可以解释这个案例了。


原型面试题

试题一

以下代码的输出结果是什么?

  1. <script>
  2. function A(){};
  3. A.prototype.n = 1;
  4. var b = new A();
  5. A.prototype = {
  6. n:2,
  7. m:3
  8. }
  9. var c = new A();
  10. console.log(b.n,b.m,c.n,c.m);
  11. </script>

答案:1 undefined 2 3。
解析
var b = new A();时,b的隐式原型其实和函数A的显示原型是相等的。
但是第5行之后,函数A的显式原型指向了一个新的对象,而b的隐式原型指向的还是原来那片空间,所以此时b的隐式原型和函数A的显式原型已经不相等了。
而此时var c = new A();的c,它的隐式原型和现在函数A新的显式原型相等了。
故输出结果时,b的隐式原型只有n这个属性,没有m这个属性。而c中n和m属性都有。


试题二

下列执行的函数中,有无法正常执行的函数吗?

  1. <script>
  2. var F = function(){};
  3. Object.prototype.a = function(){
  4. console.log('a()');
  5. }
  6. Function.prototype.b = function(){
  7. console.log('b()');
  8. }
  9. var f = new F();
  10. f.a();
  11. f.b();
  12. F.a();
  13. F.b();
  14. </script>

答案:f.b()无法正常执行。
解析
f属于F实例化后的对象,所以f的隐式原型等于F的显式原型,F的显式原型为空,所以找F显式原型的隐式原型,即Object的显式原型,然后原型链就找到尽头了,很明显在这条原型链中,a()在Object的原型中,而b()却没找到。
F属于Function实例化后的对象,所以F的隐式原型等于Function的显式原型。Function的显式原型的隐式原型又时Object的显式原型。所以对于F来说,a()和b()都是可以正常执行的。
这一题需要对原型链比较熟悉,看不懂解析的建议自己去画一下图。


执行上下文与执行上下文栈

变量提升与函数提升

先看一道简单的面试题

  1. <script>
  2. var a = 3;
  3. function fun(){
  4. console.log(a);
  5. var a = 4;
  6. }
  7. fun();
  8. </script>

答案:undefined
解析:这个的答案很明显,因为使用var声明的变量会进行提前声明,所以函数中后面执行的var a = 4;,其实在函数定义完之后,程序就默认执行了var a;,所以进行输出的时候由于还未赋值即undefined。

变量提升提升

  • 通过var定义(声明)的变量,在定义语句之前就可以访问到。
  • 值:undefinde(未定义)

函数声明提升

  • 通过function声明的函数,在定义语句之前就可以正常进行调用。
  • 值:函数定义(对象)

注意:var fun = function(){};进行的是变量提升,所以是无法提前调用的,只有function fun(){};这样形式的才行。


执行上下文

概述

  1. 代码分类(位置)
  • 全局代码
  • 函数(局部)代码
  1. 全局执行上下文
  • 在执行全局代码前将window确定为全局执行上下文。
  • 对全局数据进行预处理(即在全部代码执行之前进行处理)
    • var定义的全局变量 => undefined,添加为window的属性
    • function声明的全局函数 => 赋值(fun),添加为window的方法
    • this => 赋值(window)
  • 开始执行全局代码
  1. 函数执行上下文
  • 在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
  • 对局部数据进行预处理
    • 形参变量 => 赋值(实参) => 添加为执行上下文属性
    • arguments(是一个类数组) => 赋值(存储所有实参的数组) => 添加为执行上下文属性
    • var定义的局部变量 => undefined,添加为执行上下文属性
    • function声明的函数 => 赋值(fun),添加为执行上下文方法
    • this => 赋值(调用函数的对象)
  • 开始执行函数体代码

执行上下文栈

概述

  1. 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象。
  2. 在全局执行上下文(window)确定后,将其添加到栈中(压栈)。
  3. 在函数执行上下文创建后,将其添加到栈中(压栈)。
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)。
  5. 当所有代码执行完成后,栈中只剩下window。

我们来看这么一段代码

  1. <script> // 1. 开始全局执行上下文
  2. var a = 10
  3. var bar = function(x){
  4. var b = 5
  5. foo(x + b); // 2. 进入foo函数执行上下文
  6. }
  7. var foo = function (y){
  8. var c = 5;
  9. console.log(a+c+y);
  10. }
  11. bar(10); // 3. 进入bar函数执行上下文
  12. </script>

按照执行上下文的概念,这段代码一共会执行三次上下文。
这里要说的是执行上下文栈的概念,当程序在执行上下文对象时,它在栈中是这么运行的。
image.png
栈的特点是后进先出,所以最上层的对象一定是正在执行的对象,当上层的对象执行完毕,继续执行下一层的对象,所以栈中的执行上下文对象,永远是n+1个,n为正在执行的函数,1为window全局对象。

练习

【习题1】不执行代码,回答出下面两个问题:

  1. 程序执行完成后,控制台依次输出什么?
  2. 整个过程中产生了几个执行上下文?

    1. <script>
    2. console.log('global begin:' + i)
    3. var i = 1
    4. foo(1)
    5. function foo(i) {
    6. if (i==4) {
    7. return
    8. }
    9. console.log('foo() begin:' + i)
    10. foo(i+1);
    11. console.log('foo() end:' + i)
    12. }
    13. console.log('global end:' + i)
    14. </script>

    答案

  3. 程序执行结果如图所示

image.png

  1. 执行了5个执行上下文对象

解析:这个需要理解清楚栈的后进先出的特点,如果看不懂可以依据每一步,将压栈的过程画图表示出来。


【习题2】下面三段代码的执行结果分别是什么?

  1. <script>
  2. // 试题1
  3. function a() {}
  4. var a
  5. console.log(typeof a)
  6. //试题2
  7. if (!(b in window)) {
  8. var b = 1;
  9. }
  10. console.log(b);
  11. //试题3
  12. var c = 1
  13. function c(c){
  14. console.log(c)
  15. }
  16. c(2)
  17. </script>

答案

  • 试题1 -> ‘function’
  • 试题2 -> undefined
  • 试题3 -> 报错

解析

  • 这三道题都与变量提升或者函数提升相关,首先要明白,程序先执行变量提升,而后执行函数提升。
  • 试题1:程序先进行变量提升,最先执行var a,而后执行函数提升,function a将原来的变量a替代了。
  • 试题2:程序先执行变量提升,所以b早就是window的属性了,b in window的结果为true再取反,所以没执行赋值语句。
  • 试题3:先执行变量提升,然后函数提升将原来的变量替换成函数,之后执行var c = 1又修改回了变量,变量不是函数当然无法执行。