JS高级用法01(学习笔记)

146 阅读17分钟

JS高级用法01

课程目标

  • 学习JS原型和原型链
  • 词法作用域和动态作用域
  • 执行上下文
  • 变量对象
  • 作用域链
  • this
  • 闭包

知识要点

JS原型&原型链

prototype

prototype是只有函数才具有的属性,例如:

 function Person(){}
 Person.prototype.name = 'zhangSan';
 
 var person1 = new Person();
 var person2 = new Person();
 
 console.log(person1.name)//zhangSan
 console.log(person2.name)//zhangSan

函数的prototype属性指向了一个对象,这个对象是调用该构造函数而创建的实例的原型,也就是例子中person1person2的原型。

  • 什么是原型?

    每一个JavaScript对象(null除外)在创建的时候会关联另一个对象,这个被关联的对象就是原型,每一个对象都会从原型"继承"属性。

image.png

_proto_

每一个JavaScript对象(除了null)都具有的一个属性,_proto_,这个属性会指向该对象的原型。

 function Person() {
 }
 var person = new Person();
 console.log(person.__proto__ === Person.prototype); // true

image.png

绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于Person.prototype中,实 际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter, 当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)

constructor

每个原型都有一个 constructor 属性指向关联的构造函数

 function Person() {
 }
 console.log(Person === Person.prototype.constructor); // true

image.png

 function Person() {
 }
 var person = new Person();
 console.log(person.__proto__ == Person.prototype) // true
 console.log(Person.prototype.constructor == Person) // true
 console.log(Object.getPrototypeOf(person) === Person.prototype) // true
 function Person() {
 }
 var person = new Person();
 console.log(person.constructor === Person); // true

当获取person.constructor时,其实person并没有constructor属性,当不能读取到constructor属性时,会从person的原型Person.prototype中读取,所以:

 person.constructor === Person.prototype.constructor

实例&原型

当读取实例的属性时,如果找不到,就回查找与对相关联的原型中的属性,如果还查不到,就继续找,直到找到最顶层为止。

例:

 function Person() {
 }
 Person.prototype.name = 'zhangSan';
 var person = new Person();
 person.name = 'zhangSan';
 console.log(person.name) // zhangSan
 delete person.name;
 console.log(person.name) // zhangSan

在这个例子中,给实例对象person添加了name属性,所以打印person.name的时候,结果为zhangSan; 但是删除了person的原型也就是peison._proto_后,会继续向上层Person.prototype中查找,结果为zhangSan。

那么问题来了,如果在最顶层还是没有找到呢?原型的原型又是什么呢?

原型的原型

原型对象就是通过Obiect构造函数生成的,实例的_proto_指向的是构造函数的prototype,所以最终关系图为:

image.png

原型链

由于Object.prototype没有原型,所以属性查找到Object.prototype时,就停止查找了。 所以原型链的关系图可以更新为:

image.png

继承

继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对 象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继 承,委托的说法反而更准确些。

词法作用域和动态作用域

作用域

作用域是指程序源代码中定义为变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript采用词法作用域(静态作用域)。

静态作用域和动态作用域

因为JavaScript采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

var value = 1;
function foo(){
    console.log(value);
}
function bar(){
    var value = 2;
    foo();
}
bar();

分析上面例子的执行过程

  1. 执行foo()函数,先从foo函数内部查找是否有局部变量value,如果没有,查找上层代码,也就是window中的全局变量value = 1,所以结果为1。
  2. 执行 foo()函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用 域,也就是 bar() 函数内部查找 value 变量,所以结果会打印 2。 前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

动态作用域

bash是动态作用域

value=1
function foo () {
echo $value;
}
function bar () {
local value=2;
foo;
}
bar //结果为2

面试题:

//case1
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
   	function f(){
        return scope;
    }
    return f();
}
checkscope();//local scope

// case 2
var scope = "global scope";
function checkscope(){
	var scope = "local scope";
	function f(){
		return scope;
	}
	return f;
}
checkscope()();//local scope

JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定 义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执 行 f() 时依然有效。

执行上下文

顺序执行

var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2

JavaScript引擎并非一行一行的分析和执行程序,而是一段一段地分析执行。

可执行代码

JavaScript 的可执行代码( executable code)的类型:

  • 全局代码
  • 函数代码
  • eval代码

当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,用个更专业一点 的说法,就叫做"执行上下文( execution context)"。

执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文,栈结构遵循先入后出规则。

回顾上文面试题

两段代码的执行结果虽然相同,但是执行上下文栈的变化不一样

模拟第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

模拟第二段代码:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

变量对象

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)

每个执行上下文都有三个重要属性:

  • 变量对象(Variable object,VO);
  • 作用域链(Scope chain);
  • this;

变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。

全局上下文

  • 全局对象是预定义的对象,作为JavaScript的全局函数和全局属性的占位符。通过使用全局对象。可以访问其他所有预定义的对象、函数、属性。
  • 在顶层JavaScript代码中,可以用关键字this引用全局对象,因为全局对象是作用域链的头,所有非限定性的变量和函数名都会作为该对象的属性来查询。

简单来说:

  • 可以通过this引用,在客户端JavaScript中,全局对象就是Window对象。
  • 全局对象是由Object构造函数实例化的一个对象。
  • 全局上下文中的变量对象就是全局对象。

函数上下文

在函数上下文中,用活动对象(activation object,AO)来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上或者说是引擎上实现的,不可以在JavaScript环境中访问,只有进入一个执行上下文中,这个执行上下文的变量对象才会被激活,只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments 属性值 是 Arguments 对象。

执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  1. 进入执行上下文;
  2. 代码执行;

进入执行上下文

进入执行上下文时,这时候还没有执行代码;

变量对象会包括:

  1. 函数的所有形参(如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建;
    • 没有实参,属性值设置为undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性;
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性

    例:

    function foo(a) {
        var b = 2;
        function c() {}
        var d = function() {};
        b = 3;
    }
    foo(1);
    

    在进入执行上下文之后,这时候的AO是:

    AO = {
        arguments:{
            0:1,
            length:1
        }
        a:1,
        b:undefined,
        c:reference to function c(){},
        d:undefined
    }
    

    在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

    当代码执行完成后,AO是:

    AO = {
        arguments:{
            0:1,
            length:1
        }
        a:1,
        b:2,
        c:reference to function c(){},
        d:reference to FunctionExpression "d"
    }
    

    总结:

    1. 全局上下文的变量初始化是全局对象;
    2. 函数上下文的变量初始化只包含Arguments对象;
    3. 在进入执行上下文的时候会给变量对象添加形参、函数声明、变量声明等初始的属性值;
    4. 在代码执行阶段,会再次修改变量的属性值;

    思考题

    example 1

     function foo() {
    	console.log(a);
    	a = 1;
     }
     foo(); // ???
     function bar() {
    	a = 1;
    	console.log(a);
     }
    bar(); // ???
    

    第一段会报错: Uncaught ReferenceError: a is not defined。 第二段会打印:1。 这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。 第一段执行 console 的时候, AO 的值是:

    AO = {
        arguments: {
            lengthL 0
        }
    }
    

    没有a的值,去全局找,全局也没有,所以报错

    当第二段执行console时,全局对象已经被赋给了a属性,可以从全局找到a的值,所以会打印1

    example 2

    console.log(foo);
     function foo(){
         console.log("foo");
     }
    var foo = 1;
    

    会打印函数,而不是undefined

    因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明 的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法 层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样 由多个执行上下文的变量对象构成的链表就叫做作用域链。

函数的作用域

函数的作用域在函数定义的时候就决定了。

因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以 理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO  
];
bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函数激活

函数激活时。进入函数上下文,创建AO/VO后,就会将活动对象添加到作用域链的最前端。

这时候执行上下文的作用域链,命名为Scope

Scope = [AO].concat([[Scope]]);

作用域链创建完毕

总结

总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
	var scope2 = 'local scope';
	return scope2;
}
checkscope();

执行过程如下:

  1. checkscope 函数被创建,保存作用域链到内部属性[[scope]]

    checkscope.[[scope]] = [
        gloablContext.VO
    ];
    
  2. 执行checkscope函数,创建checkscope函数执行上下文,checkscope函数执行上下文被压入执行上下文栈

    ECStack = [
        checkscopeContext,
        globalContext
    ];
    
  3. checkscope函数并不是立即执行,第一步:复制函数[[scope]]属性创建作用域链

    checkscopeContext = {
        Scope:checkscope.[[scope]]
    }
    
  4. 第二步:用arguments创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

  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. 查找到scope2的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

    ECStack = [
        globalContext 
    ];
    

this

ECMAScript 的类型分为语言类型和规范类型。 ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。 而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型 的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。 我们只要知道在 ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层 行为逻辑。 这里要讲的重点是便是其中的 Reference 类型。它与 this 的指向有着密切的关联。

Reference

Reference 类型就是用来解释诸如 deletetypeof 以及赋值等操作行为的。

Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好 地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。

Reference 的构成,由三个组成部分,分别是:

  • base value
  • referenced name
  • strict reference

base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。 referenced name 就是属性的名称。

var foo = 1;
//对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

var foo = {
    bar: function(){
        return this;
    }
};

foo.bar();

//bar对应的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

规范中还提供了获取 Reference 组成部分的方法,比如 GetBaseIsPropertyReference

GetBase

返回 referencebase value

var foo = 1;
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict:false
};
GetValue(fooReference) //1

GetValue 返回对象属性真正的值,但是,调用 GetValue,返回的将是具体的值,而不再是一个 Reference

IsPropertyReference

如果 base value 是一个对象,就返回true

如何确定this的值

  1. 计算 MemberExpression 的结果赋值给 ref

  2. 判断ref是不是一个 Reference 类型; a. 如果refReference,并且 IsPropertyReference(ref) true, 那么 this 的值为 GetBase(ref) b. 如果 ref Reference,并且base value值是 Environment Record, 那么this的值为 ImplicitThisValue(ref) c. 如果 ref 不是 Reference,那么 this 的值为 undefined

具体分析

计算 MemberExpression 的结果赋值给 ref

MemberExpression :

  • PrimaryExpression // 原始表达式

  • FunctionExpression // 函数定义表达式

  • MemberExpression [ Expression ] // 属性访问表达式

  • MemberExpression . IdentifierName // 属性访问表达式

  • new MemberExpression Arguments // 对象创建表达式

function foo(){
    console.log(this)
}
foo();//MemberExpression是foo

function foo(){
    return  function(){
        console.log(this)
    }
}
foo()();//MemberExpression是foo()

var foo = {
    bar: function(){
        return this;
    }
}
foo.bar();//MemberExpression是foo.bar

简单理解 MemberExpression 其实就是()左边的部分。

判断 ref 是不是一个 Reference 类型

看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。

var value = 1;
var foo = {
	value: 2,
	bar: function () {
		return this.value;
	}
}
//示例1
console.log(foo.bar());//2
//示例2
console.log((foo.bar)());//2
//示例3
console.log((foo.bar = foo.bar)());//1
//示例4
console.log((false || foo.bar)());//1
//示例5
console.log((foo.bar, foo.bar)());//1

以上是在非严格模式下的结果,严格模式下因为 this 返回 undefined,所以示例 3 会报错。

示例1 foo.bar()

上面的demo中, MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢?

var Reference = { 1 base: foo, name: 'bar', strict: false };

如果 ref Reference,并且 IsPropertyReference(ref)true, 那么this的值为 GetBase(ref)该值是 Reference 类型,那么IsPropertyReference(ref)的结果是多少呢?

前面我们说了IsPropertyReference 方法,如果 base value 是一个对象,结果返回 truebase value foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。 这个时候我们就可以确定 this 的值:

this = GetBase(ref)

GetBase 也已经铺垫了,获得 base value 值,这个例子中就是foo,所以 this 的值就是foo,示例1的结 果就是 2。

示例2 (foo.bar)()

console.log((foo.bar)());

foo.bar 被 () 包住

实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的。

示例3 (foo.bar = foo.bar)()

示例3,有赋值操作符, 因为使用了 GetValue,所以返回的值不是 Reference 类型, 按照之前讲的判断逻辑,如果 ref 不是Reference,那么 this 的值为 undefined thisundefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。

示例4 (false || foo.bar)()

因为使用了 GetValue,所以返回的不是Reference类型,this undefined

示例5 (foo.bar, foo.bar)()

因为使用了 GetValue,所以返回的不是 Reference 类型,this undefined

闭包

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。 但是,这是理论上的闭包,其实还有一个实践角度上的闭包。 ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简 单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外 层的作用域;
  2. 从实践角度:以下函数才算是闭包: a. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回); b. 在代码中引用了自由变量;
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 函数上下文从执行上下文栈中弹出;

实践角度上闭包的定义:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回);
  2. 在代码中引用了自由变量;

补充知识

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。 data[1] 和 data[2] 是一样的道理。

所以改成闭包:

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]
}

匿名函数执行上下文的AO为:

匿名函数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。 data[1]data[2] 是一样的道理。