原型与原型链
- 函数的prototype属性
- 每个函数都有prototype属性,它默认指向一个Object空对象(即称为:原型对象)。
- 原型对象中有一个属性constructor,它指向函数对象。
- 给原型对象添加属性(一般都是方法)
- 原型中的方法都是给实例对象使用的
- 作用:函数的所有实例对象自动拥有原型中的属性(方法)。
示例:
<script>
console.log(Date.prototype,typeof Date.prototype);
//创建一个函数
function Fun(){
}
console.log(Fun.prototype);
//原型中的constructor属性指向函数对象
console.log(Date.prototype.constructor === Date); //true
console.log(Fun.prototype.constructor === Fun); //true
Fun.prototype.test = function(){
console.log('test()');
}
var fun = new Fun();
//调用原型中的方法
fun.test();
</script>
显式原型和隐式原型
概念:
- 每个函数function都有一个prototype,即显式原型(属性)。
- 每个实例对象都有一个proto,即隐式原型(属性)。
- 实例对象的隐式原型的值等同于其对应构造函数显式原型的值。
示例:
<script>
function Fun() { };
// 1. 每个函数function都有一个prototype,即显式原型(属性)。
console.log(Fun.prototype);
// 2. 每个实例对象都有一个__proto__,即隐式原型(属性)。
var fun = new Fun();
console.log(fun.__proto__);
// 3. 实例对象的隐式原型的值等同于其对应构造函数显式原型的值。
console.log(Fun.prototype === fun.__proto__);
</script>
那么函数的prototype属性是怎么来的呢?我们声明函数的时候没有定义过呀。所以肯定是引擎加的,那什么时候加的呢,函数对象一创建,引擎就会自动帮我们加上prototype属性了,只不过指向一个空对象,相当于函数内部默认执行了this.prototype = {};
那么实例对象的proto属性是怎么来的呢?肯定也是引擎帮我们加的,而且是一创建实例化对象的时候就加上了,并且实例对象的proto属性和构造函数prototype是一样的,相当于执行了一条内部语句:this.__proto__ = Fun.prototype;
依据以上的知识我们可以画出大概的内存图:
总结:
- 函数的prototype属性:在定义函数时自动添加,默认值是空Object对象。
- 对象的proto属性:创建对象时自动添加,默认值为对应构造函数的prototype属性值。
- 程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)。
原型链
概述
- 访问一个对象的属性时:
- 先在自身属性中查找,找到返回。
- 如果没有,再沿着proto这条链向上查找,找到返回。
- 如果最终没找到,返回undefined。
- 原型链别名:隐式原型链
- 作用:查找对象的属性(方法)
- Object原型对象是原型对象的终点,任何函数都可以顺着原型链往上查找到Object原型对象,所以任何函数可以使用Object原型对象中定义的方法。
图解
构造函数/原型/实例对象的关系
任何对象的隐式原型等于Object的显式原型
任何函数的隐式原型等于Function的显式原型
补充
函数的显示原型指向的对象:默认是空Object对象(但Object不满足)
<script>
function Fn(){}
console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true
</script>
所有函数都是Function的实例(包括Function本身)
console.log(Function.prototype === Function.__proto__) // true
Object的原型对象是原型链的尽头
console.log(Object.prototype.__proto__) //null
属性问题
- 读取对象的属性值时:会自动到原型链中查找
- 设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加次属性并设置其值
- 方法一般定义再原型中,属性一般通过构造函数定义在对象本身上
示例
<script>
function Fn(){}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a)
var fn2 = new Fn()
fn2.a = 'yyy' //这里修改的只是对象自身的属性,并不会修改原型中的a属性
console.log(fn1.a,fn2.a)
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function(name){
this.name = name
}
var p1 = new Person('张三',50)
p1.setName('李四 ')
console.log(p1)
var p2 = new Person('王五',17)
console.log(p1.__proto__ === p2.__proto__)
</script>
探索instanceof
- instanceof是如何判断的?
- 表达式:A instanceof B (A是实例对象,B是构造函数对象)
- 如果B函数的显示原型对象在A对象的原型链上,返回true,否则返回false
案例1
<script>
function Fn(){}
var fn1 = new Fn()
console.log(fn1 instanceof Fn) //true
console.log(fn1 instanceof Object) //true
</script>
图示
案例2
<script>
console.log(Object instanceof Function) //true
console.log(Object instanceof Object) //true
console.log(Function instanceof Function) //true
console.log(Function instanceof Object) //true
function Foo(){}
console.log(Object instanceof Foo) //false
</script>
此处不在画图示,其实”构造函数/原型/实例对象的关系”中的第二副图,基本以及可以解释这个案例了。
原型面试题
试题一
以下代码的输出结果是什么?
<script>
function A(){};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n:2,
m:3
}
var c = new A();
console.log(b.n,b.m,c.n,c.m);
</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属性都有。
试题二
下列执行的函数中,有无法正常执行的函数吗?
<script>
var F = function(){};
Object.prototype.a = function(){
console.log('a()');
}
Function.prototype.b = function(){
console.log('b()');
}
var f = new F();
f.a();
f.b();
F.a();
F.b();
</script>
答案:f.b()无法正常执行。
解析:
f属于F实例化后的对象,所以f的隐式原型等于F的显式原型,F的显式原型为空,所以找F显式原型的隐式原型,即Object的显式原型,然后原型链就找到尽头了,很明显在这条原型链中,a()在Object的原型中,而b()却没找到。
F属于Function实例化后的对象,所以F的隐式原型等于Function的显式原型。Function的显式原型的隐式原型又时Object的显式原型。所以对于F来说,a()和b()都是可以正常执行的。
这一题需要对原型链比较熟悉,看不懂解析的建议自己去画一下图。
执行上下文与执行上下文栈
变量提升与函数提升
先看一道简单的面试题
<script>
var a = 3;
function fun(){
console.log(a);
var a = 4;
}
fun();
</script>
答案:undefined
解析:这个的答案很明显,因为使用var声明的变量会进行提前声明,所以函数中后面执行的var a = 4;
,其实在函数定义完之后,程序就默认执行了var a;
,所以进行输出的时候由于还未赋值即undefined。
变量提升提升:
- 通过var定义(声明)的变量,在定义语句之前就可以访问到。
- 值:undefinde(未定义)
函数声明提升:
- 通过function声明的函数,在定义语句之前就可以正常进行调用。
- 值:函数定义(对象)
注意:var fun = function(){};
进行的是变量提升,所以是无法提前调用的,只有function fun(){};
这样形式的才行。
执行上下文
概述
- 代码分类(位置)
- 全局代码
- 函数(局部)代码
- 全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文。
- 对全局数据进行预处理(即在全部代码执行之前进行处理)
- var定义的全局变量 => undefined,添加为window的属性
- function声明的全局函数 => 赋值(fun),添加为window的方法
- this => 赋值(window)
- 开始执行全局代码
- 函数执行上下文
- 在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
- 对局部数据进行预处理
- 形参变量 => 赋值(实参) => 添加为执行上下文属性
- arguments(是一个类数组) => 赋值(存储所有实参的数组) => 添加为执行上下文属性
- var定义的局部变量 => undefined,添加为执行上下文属性
- function声明的函数 => 赋值(fun),添加为执行上下文方法
- this => 赋值(调用函数的对象)
- 开始执行函数体代码
执行上下文栈
概述
- 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象。
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈)。
- 在函数执行上下文创建后,将其添加到栈中(压栈)。
- 在当前函数执行完后,将栈顶的对象移除(出栈)。
- 当所有代码执行完成后,栈中只剩下window。
我们来看这么一段代码
<script> // 1. 开始全局执行上下文
var a = 10
var bar = function(x){
var b = 5
foo(x + b); // 2. 进入foo函数执行上下文
}
var foo = function (y){
var c = 5;
console.log(a+c+y);
}
bar(10); // 3. 进入bar函数执行上下文
</script>
按照执行上下文的概念,这段代码一共会执行三次上下文。
这里要说的是执行上下文栈的概念,当程序在执行上下文对象时,它在栈中是这么运行的。
栈的特点是后进先出,所以最上层的对象一定是正在执行的对象,当上层的对象执行完毕,继续执行下一层的对象,所以栈中的执行上下文对象,永远是n+1个,n为正在执行的函数,1为window全局对象。
练习
【习题1】不执行代码,回答出下面两个问题:
- 程序执行完成后,控制台依次输出什么?
整个过程中产生了几个执行上下文?
<script>
console.log('global begin:' + i)
var i = 1
foo(1)
function foo(i) {
if (i==4) {
return
}
console.log('foo() begin:' + i)
foo(i+1);
console.log('foo() end:' + i)
}
console.log('global end:' + i)
</script>
答案
程序执行结果如图所示
- 执行了5个执行上下文对象
解析:这个需要理解清楚栈的后进先出的特点,如果看不懂可以依据每一步,将压栈的过程画图表示出来。
【习题2】下面三段代码的执行结果分别是什么?
<script>
// 试题1
function a() {}
var a
console.log(typeof a)
//试题2
if (!(b in window)) {
var b = 1;
}
console.log(b);
//试题3
var c = 1
function c(c){
console.log(c)
}
c(2)
</script>
答案:
- 试题1 -> ‘function’
- 试题2 -> undefined
- 试题3 -> 报错
解析:
- 这三道题都与变量提升或者函数提升相关,首先要明白,程序先执行变量提升,而后执行函数提升。
- 试题1:程序先进行变量提升,最先执行var a,而后执行函数提升,function a将原来的变量a替代了。
- 试题2:程序先执行变量提升,所以b早就是window的属性了,b in window的结果为true再取反,所以没执行赋值语句。
- 试题3:先执行变量提升,然后函数提升将原来的变量替换成函数,之后执行var c = 1又修改回了变量,变量不是函数当然无法执行。