原型&原型链;
- 构造函数Person()
- 实例person
- 实例的原型Person.prototype
关系:实例.__ proto__=实例的原型=构造函数.prototype
Object提供方法getPrototypeof,访问对象的属性.Object.getPrototypeof(person)===person.__ proto __
绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.__proto__时,可以理解成返回了 Object.getPrototypeOf(obj)。
Person.prototype,也是一个对象,是一个实例。所以Person.prototype的_proto_指向Object.prototype
Object.prototype.proto==null,原型链的终结
原型链能干啥?
- 原型对象上统一添加修改属性,实例都能访问
- 实例可以一直往上找属性
- 继承:每个对象都会从原型上继承(引用委托)属性(不是原型上的属性复制赋值一份给子类,只有一份,去父类往上找)
词法作用域&动态作用域;
作用域:是指程序源代码中定义变量的区域。在作用域里获得变量的访问权限
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
- 静态:函数的作用域在函数定义时就确定了,js是静态作用域(词法作用域)
- 动态:函数的作用域在函数调用时就确定
var value=1;
function foo(){
console.log(value) //输出1。在函数定义的时候,确定value
}
function bar(){
var value=2
foo()
}
bar()
// case 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// case 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
都输出 local scope
Q:函数定义的时候,形参也可以被找吗,答:可以
有什么不同,执行上下文顺序不同
执行上下文;
执行上下文(随着代码的执行,创造的环境)
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
console.log(add2(1,1)); //输出2
function add2(a,b){
return a+b;
}
console.log(add1(1,1)); //报错:add1 is not a function
var add1 = function(a,b){
return a+b;
}
console.log(add2(1,1)); //输出2
function add2(a,b){
return a+b;
}
console.log(add1(1,1)); //报错:add1 is not a function
var add1 = function(a,b){
return a+b;
}
- 用函数语句创建的函数add2,函数名称和函数体均被提前,在声明它之前就使用它。
- 但是使用var表达式定义函数add1,只有变量声明提前了,变量初始化代码仍然在原来的位置,没法提前执行。
可执行代码包含
- 全局代码
- 函数代码
- eval(不怎么用了)
执行上下文包含
1变量对象
2作用域链
3this:始终指向调用它的内容
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
- 最先遇到的就是全局代码,初始化的时候首先就会向执行上下文栈压入一个全局执行上下文
- 当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈
- 当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出
- 最后弹出全局执行上下文
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
//伪代码执行过程:
ECStack = [];
ECStack = [
globalContext//压入全局执行上下文
];
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);
// 擦,fun2还调用了fun3!
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
// case 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// case 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
//伪代码执行过程:case 1
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
//伪代码执行过程:case 1
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
变量对象;
存储的是变量和函数声明
- 全局上下文 variable object 变量对象VO
- 函数上下文 activaion object 活动对象AO(也是变量对象,不过是进入函数执行上下文才被激活)
- 进入执行上下文(定义的时候)先定义
- 代码执行
function foo(a){
var b=2;
function c(){}
var d=function(){}
b=3
}
//定义时候
AO={
arguments:{
0:1,
length:1
},
a:1,
b:undefined,
c:referenced to function c(){}, //所以函数的调用可以在函数声明之前,定义时候引向函数了 函数提升
d:undefined //这个不行,因为此时d的值是undefined var变量提升
}
//代码执行阶段
AO={
arguments:{
0:1,
length:1
},
a:1,
b:3,
c:referenced to function c(){}, //所以函数的调用可以在函数声明之前,定义时候引向函数了
d:reference to FunctionExpression "d"
}
function foo() {
console.log(a);
a = 1;
}
foo(); // 报错,Uncaught ReferenceError: a is not defined
function foo2() {
console.log(a);
var a = 1;
}
foo(); // 不报错,输出 undefined
function bar() {
a = 1; // 不需要定义var也可以执行=>,等价于在全局定义了a=1,全局对象被赋予了 a 属性
console.log(a);
}
bar(); // 输出1
//有var执行前声明,没有var执行时声明
作用域链;
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
function foo() {
function bar() {
...
}
}
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
总结分析:函数执行上下文中作用域链和变量对象的创建过程;
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
前景,ECStack = [ globalContext ]; 压入全局上下文
- 函数checkscope被创建,保存父级作用域链到函数内部属性 [[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
2.创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
3.函数checkscope开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {//执行上下文添加作用域链
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {//执行上下文添加作用域链
AO:{
arguments:{
length:0
},
scope2:undefined
}
Scope: checkscope.[[scope]],
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {//执行上下文添加作用域链
AO:{
arguments:{
length:0
},
scope2:undefined
}
Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值 (进入函数执行阶段)
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
this;
始终指向调用它的人
function foo(){
console.log(this.a);
}
var obj = {
a : 10,
foo : foo
}
foo(); // undefined
obj.foo(); // 10
如果是链性的关系,比如 xx.yy.obj.foo();, 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个。
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
- this绑定的是新创建的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫foo的新创建的对象
, 然后将这个对象赋给bar , 这样的绑定方式叫 new绑定 - this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是
obj , 这样的绑定方式叫 显性绑定 . - this绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的
this 就是 obj . 这样的绑定方式叫 隐性绑定 . - function foo(){…} foo() ,foo 中的 this 就是window.(严格模式下默认绑定到undefined). 这样的绑定方式叫 默认绑定 .
function foo() {
getName = function () { console.log (1); };
return this;
}
foo.getName = function () { console.log(2);};
foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName () { console.log(5);}
foo.getName (); // ?
getName (); // ?
foo().getName (); // ?
getName (); // ?
new foo.getName (); // ?
new foo().getName (); // ?
new new foo().getName (); // ?
答案:2 4 1 1 2 3 3
解析:考点 1. new绑定 2.隐性绑定 3. 默认绑定 4.变量污染
function foo() {
getName = function () { console.log (1); };
//这里的getName 将创建到全局window上
return this;
}
foo.getName = function () { console.log(2);};
//这个getName和上面的不同,是直接添加到foo上的
foo.prototype.getName = function () { console.log(3);};
// 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上
var getName = function () { console.log(4);};
// 和foo函数里的getName一样, 将创建到全局window上
function getName () { console.log(5);}
// 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换
// 这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式
// 之后,所以这个函数可以忽略了
// 通过上面对 getName的分析基本上答案已经出来了
foo.getName (); // 2
// 下面为了方便,我就使用输出值来简称每个getName函数
// 这里有小伙伴疑惑是在 2 和 3 之间,觉得应该是3 , 但其实直接设置
// foo.prototype上的属性,对当前这个对象的属性是没有影响的,如果要使
// 用的话,可以foo.prototype.getName() 这样调用 ,这里需要知道的是
// 3 并不会覆盖 2,两者不冲突 ( 当你使用new 创建对象时,这里的
// Prototype 将自动绑定到新对象上,即用new 构造调用的第二个作用)
getName (); // 4
// 这里涉及到函数提升的问题,5 会被 4 覆盖,
foo().getName (); // 1
// 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,
// 2. 返回window , 故等价于 window.getName(); 输出 1
getName (); // 1
// 刚刚上面的函数刚把window.getName设置为1,故同上 输出 1
new foo.getName (); // 2
// new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊
// 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新
// 创建的对象但是我们可以猜到,是一个函数名为 foo.getName 的对象
// 且__proto__属性里有一个getName函数,是上面设置的 3 函数)
new foo().getName (); // 3
// 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它
// 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();
// obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的
// 那个getName 3 ,因为使用new后会将函数的prototype继承给 新对象
new new foo().getName (); // 3
// 分解一下:
// var obj = new foo();
// var obj1 = new obj.getName();
// 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3
// obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象
箭头函数不使用我们上面介绍的四种绑定,而是完全根据外部作用域来决定this
闭包思考题;
定义:能够访问自由变量的函数
自由变量:既不是函数的参数,也不是函数的局部变量
闭包 = 函数 + 函数能够访问的自由变量
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
这里直接给出简要的执行过程:
- 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈;
- 全局执行上下文初始化;
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈;
- checkscope 执行上下文初始化,创建变量对象、作用域链、this等;
- checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出;
- 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈;
- f 执行上下文初始化,创建变量对象、作用域链、this等;
- f 函数执行完毕,f 函数上下文从执行上下文栈中弹出;
了解到这个过程,我们应该思考一个问题: - 当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案是都是 3
分析:当执行到 data[0] 函数之前,此时全局上下文的 VO 为
globalContext = {
VO: {
data: [...],
i: 3
}
}
当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。
改成闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
跟没改之前一模一样。
当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。