1、this指针/闭包/作用域

原型&原型链;

  • 构造函数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=1function 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,只有变量声明提前了,变量初始化代码仍然在原来的位置,没法提前执行。

可执行代码包含

  1. 全局代码
  2. 函数代码
  3. eval(不怎么用了)

执行上下文包含
1变量对象
2作用域链
3this:始终指向调用它的内容

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

  1. 最先遇到的就是全局代码,初始化的时候首先就会向执行上下文栈压入一个全局执行上下文
  2. 当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈
  3. 当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出
  4. 最后弹出全局执行上下文
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(也是变量对象,不过是进入函数执行上下文才被激活)
  1. 进入执行上下文(定义的时候)先定义
  2. 代码执行
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 ]; 压入全局上下文

  1. 函数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();

这里直接给出简要的执行过程:

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈;
  2. 全局执行上下文初始化;
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈;
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等;
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出;
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈;
  7. f 执行上下文初始化,创建变量对象、作用域链、this等;
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出;
    了解到这个过程,我们应该思考一个问题:
  9. 当 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。

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值