本书主要从两个部分来进行阐述:作用域与闭包、this与对象原型。
一 作用域与闭包
1— 作用域
对JavaScript而言,大部分情况下编译发生在代码执行前的几微妙的时间内。在我们所讨论的作用域背后,JavaScript引擎为保证性能最佳做出了努力,如JIT,可以延迟编译或重编译。
引擎=>从头到尾负责整个JavaScript程序的编译及执行过程
编译器=>负责语法分析及代码生成等
作用域=>负责收集并维护由所有声明的标识符组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值:首先编译器会在当前作用域中声明一个变量(如果之前没有声明过);然后在运行时引擎会在作用域中查找该变量,找到就对其进行赋值。
LHS:当变量出现在赋值操作的左侧时进行LHS查询。赋值操作的目标是谁(LHS)
RHS:当变量出现在赋值操作的右侧时进行RHS查询。谁是赋值操作的源头(RHS)
作用域是根据名称查找变量的一套规则,用于确定在何处以及如何查找变量。如果查找的目的是对变量进行赋值,则执行LHS查询;如果查找的目的是获取变量的值,则执行RHS查询。赋值操作会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。当一个函数嵌套在另一个函数中时,就发生了作用域的嵌套。因此,当当前作用域中找不到某个变量时,引擎就会在其嵌套的外层作用域中继续查找,直到找到该变量或者到达最外层作用域为止。
不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量会使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。
2— 词法作用域
词法作用域:定义在词法阶段的作用域,即由你在写代码时将变量和函数作用域写在哪里决定,即函数或变量声明时所处的位置决定。
3— 函数作用域
函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来隐藏它们。这可以避免同名标识符间的冲突,也可以起到保护内部变量和函数的作用。
函数声明:function是声明中的第一个词;函数表达式不是以function开头。函数声明与函数表达式的主要区别在于它们的名称标识符将会绑定在何处。
匿名函数表达式:没有名称标识符。缺点:匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难;不利于自身调用;可读性不友好。Ps:始终给函数表达式命名为最佳实践。
立即执行函数:函数被包含在一对()括号内部,由此成了一个表达式,通过在末尾加上另外一个()可以立即执行该函数。
立即执行函数的两种书写方式:1. (function(){…})() 2. (function(){…}())
Ps:立即执行函数可以指定一个函数名,也可以传入参数。
Ps:任何声明在某个作用域内的变量,都将附属于这个作用域。
4— 提升
函数和变量声明从它们在代码中出现的位置被移动到了所在你作用域的最顶端,这个过程叫作提升。
函数声明与变量声明都会被提升,ps:函数声明先被提升,变量声明后被提升。
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会被提升。
5— 作用域闭包
闭包:函数在它本身的词法作用域之外执行的,或者,可以通过闭包发现当前词法作用域之外的变量。
function foo(){ var a=2; function bar(){ console.log(a); } return bar; } var baz=foo(); baz();
ps: 在定时器、时间监听器、ajax请求、跨窗口通信、Web Workers或者其他异步或同步任务中,只要使用了回调函数,实际上是在使用闭包。
for(var i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i*1000); }
这段代码会在运行时以每秒一次的频率输出5次6.该循环终止的条件是i不再<=5,条件首次成立时i的值为6,因此最终循环结束时i值为6.尽管循环中的五个函数是在各自迭代中分别定义的,但它们都被封闭在一个共享的全局作用域中,实际上只有一个i。为了依次输出1到5,有以下几种方法:
(1)需要自己的变量,用来在每个迭代中存储i的值 for(var i=1;i<=5;i++){ (function(j){ var j=I; setTimeout(function timer(){ console.log(j); } ,j*1000); })( ); } (2) for(var i=1;i<=5;i++){ (function(j){ setTimeout(function timer(){ console.log(j); },j*1000); })(i); } Ps:在迭代中使用立即执行函数为每一个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。 (3) for(var i=1;i<=5;i++){ let j=i; setTimeout(function timer(){ console.log(j); },j*1000); } (4) for(let i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i*1000); }
Ps:let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。本质上,将一个块转换成一个可以关闭的作用域。
Ps: for循环中调用setTimeout()函数,延迟函数的回调会在循环结束时才执行。
二 this与对象原型
1— 关于this
this是在运行时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录,也称执行上下文,这个记录会包含函数在哪里被调用、调用的方式、传入的参数等。
this是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
2— this全面解析
调用位置就是函数在代码中被调用的位置。寻找调用位置就是寻找函数被调用的位置。最重要的是要分析调用栈。
function baz(){ //当前调用栈是baz //当前调用位置是全局作用域 console.log(“baz”); bar(); //bar的调用位置 } function bar(){ //当前调用栈是baz->bar //当前调用位置在baz中 console.log(“bar”); foo(); //foo的调用位置 } function foo(){ //当前调用栈是baz->bar->foo //当前调用位置在bar中 console.log(“foo”); } baz(); //baz的调用位置
函数的执行过程中调用位置如何决定this的绑定对象,这里需要先找到调用位置,然后判断应用哪种绑定规则。
绑定规则:默认绑定、隐式绑定、显示绑定、new绑定
默认绑定:独立函数调用
function foo(){ console.log(this.a); } var a=2; foo(); //2
隐式绑定:需要考虑调用位置是否有上下文对象,或者,是否被某个对象拥有或包含。隐式绑定会把函数调用中的this绑定到这个上下文对象。在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。
function foo(){ console.log(this.a); } var obj={ a: 2, foo: foo }; obj.foo(); //2
ps:对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
function foo(){
console.log(this.a);
}
var obj2={
a:42,
foo:foo
};
var obj1={
a:2,
obj2:obj2
};
Obj1.obj2.foo(); //42
显示绑定:可以使用call或apply方法为调用函数指定上下文,直接指定this的绑定对象。
function foo(){ console.log(this.a); } var obj={ a:2 }; foo.call(obj); //2
new绑定:使用new来调用函数,或者说发生构造函数调用,会自动执行下面的操作=》(1)创建一个全新的对象 (2)这个对象会被执行[[Prototype]]连接 (3)这个心对象会绑定到函数调用的this (4)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){ this.a=a; } var bar=new foo(2); console.log(bar.a); //2
绑定的优先级比较:new绑定》显示绑定》隐式绑定》默认绑定
(1) 函数是否由new调用?是的话this绑定到新创建的对象。
var bar=new foo();
(2) 函数是否通过call或apply或硬绑定调用?是则this绑定到指定的对象。
var bar=foo.call(obj2);
(3) 函数是否在某个上下文中调用?是则this绑定到那个上下文。
var bar=obj1.foo();
(4) 以上都不是的话,则是默认绑定。
3— 对象
对象定义方式两种:声明形式、构造形式
声明形式: var myObj={ key:value } 构造形式: var myObj=new Object(); myObj.key=value;
二者区别:声明形式中可以添加多个键/值对,但在构造形式中需要逐个添加属性。
类型:string、number、boolean、null、undefined、object
内置对象:String、Number、Boolean、Object、Function、Array、Date、RegExp、Error
Ps: 这些内置函数可以当做构造函数使用,可以构造一个对应子类型的新对象。
Ps:必要时,JavaScript会自动把字符串字面量转换为String对象。
Ps:null与undefined没有对应构造形式,执行声明形式;Date只有构造形式,没有声明形式。
Ps:对于Object、Array、Function、RegExp而言,无论是声明形式还是构造形式,它们都是对象。
内容:对象的内容是由一些存储在特定命名位置的值组成的,我们称之为属性。存储在对象容器内部的是这些属性的名称,它们像指针一样指向这些值真正的存储位置。
属性值的访问方式:. 操作符 或者 [ ]操作符
其中,.a 被称为属性访问,[“a”] 被称为键访问。它们访问的是同一个位置,且返回相同的值。在对象中,属性名永远都是字符串。虽然在数组下标中使用的是数字,但在对象属性名中数字会被转换成字符串。
可计算属性名:在文字形式中使用[ ]包裹一个表达式来当作属性名。
var prefix=”foo”; var myObj={ [prefix + “bar”]: ”hello”, [prefix + “baz”]: “world” }; myObj[“foobar”]; //hello myObj[“foobaz”]; //world
数组:
var arr=["foo","42","bar"]; arr.baz="baz"; console.log(arr.length); //3 console.log(arr.baz); //baz var arr1=["foo",42,"bar"]; arr1["3"]="baz"; console.log(arr1.length); //4 console.log(arr1[3]); //baz
ps:因为数组本身就是一个对象,你通过arr['name'] 或者 arr.name添加的都是对象的成员 而不是给数组添加成员。数组只有通过arr[num] ,num为自然数,才能够添加或访问数组的成员。
属性描述符:可以通过Object.defineProperty(..)来添加一个新属性或者修改一个已有属性,并对属性进行设置。
var myObj={}; Object.defineProperty(myObj,”a”,{ value:2, writeable:true, configurable:true, enumerable:true }); myObj.a; //2
不变性:属性或者对象的不可改变性可通过多种方法实现。所有方法创建的都是浅不变性,即它们只会影响目标对象和它的直接属性。
(1) 对象常量,writable与configurable设置为false。不可修改、重定义或者删除
(2) 禁止扩展,Object.preventExtensions(myObj);
(3) 密封,Object.seal( ..)会创建一个密封的对象,该方法实际上是会在一个现有对象上调用Object.preventExtensions( ..)并把所有现有属性标记为configurable为false。密封之后不能添加新属性,也不能重新配置或删除任何现有属性,可修改属性值。
(4) Obj.preventExtensions()(){})冻结,Object.freeze(..)会创建一个冻结对象,该方法实际上会在一个现有对象上调用Object.seal(..)并把所有数据访问属性标记为writable为false。禁止对对象本身及其任意直接属性的修改。
深度冻结方法:先在该对象上调用Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用Object.freeze(..)深度深度
[[GET]] 获取属性值 [[PUT]]设置属性值
引用对象的属性时会触发GET操作,设置对象属性时会触发PUT操作。
遍历:
for…in 循环可以用来遍历对象的可枚举属性列表,通过遍历下标来指向值。
forEach(…)遍历数组中的所有值并忽略回调函数的返回值
every(…)会一直运行直到回调函数返回false。
some(…)会一直运行直到回调函数返回true。
for…of 首先会向被访问对象请求一个迭代器对象,然后通过迭代器对象的next()方法来遍历所有的值。
var myarr=[1,2,3]; for(var v of myarr){ console.log(v); }
var myarr=[1,2,3]; var it=myarr[Symbol.iterator](); it.next(); // {value:1, done:false} it.next(); // {value:2, done:false} it.next(); // {value:3, done:false} it.next(); // {done:true}
ES6中使用Symbol.iterator来获取对象的@@iterator内部属性。@@iterator本身不是一个迭代器对象,而是一个返回迭代器对象的函数。value表示当前遍历值,done表示十分还有可以遍历的值。for…of循环每次调用的myObj迭代器对象的next()方法时,内部的指针都会向前移动并返回对象属性列表的下一个值。
4— 混合对象“类”
5— 原型
JavaScript对象有一个特殊的prototype内置属性,本质上是对其他对象的引用。
[[Prototype]]机制指对象中的一个内部链接引用另一个对象。其本质是对象之间的关联关系。如果在第一个对象上没有找到需要的属性或方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找,如果后者没有找到,则继续查找后者的[[Prototype]],直到找到或者到达Object.prototype。这一系列对象的链接被称为原型链。所有普通对象都有内置Object.prototype,指向原型链的顶端。
关联两个对象最常用的方式是使用new关键词进行函数调用。使用new调用函数时,会把新对象的.prototype属性关联到其他对象。带new的函数调用通常被称为构造函数调用。
原型,本质是一个对象,其他对象可以通过它实现属性继承。所有对象在默认情况下都有一个原型。多个实例共享一个通用原型。原型属性一旦定义,就可以被多个引用它的实例所继承。
Prototype是构造函数的一个属性,该属性指向一个对象,而这个对象将作为该构造函数所创建的所有实例的基引用,可以把对象的基引用想象成一个自动创建的隐藏属性,当访问对象的一个属性时,首先查找对象本身,找到则返回,若没有,则查找其基引用指向的对象的属性,这样一直沿着原型链向上查找,直到找到或者到达根。
原型链:由一些用来继承和共享属性的对象组成的对象链。让原型对象等于另一个构造函数的实例,这样原型对象将包含一个指向另一个原型的指针。相应地,另一个原型中也包含指向另一个构造函数的指针。假如另一个原型又是另一个构造函数的实例,层层递进,构成了实例与原型间的链条,即原型链。
简单来讲,每个对象和原型都有一个原型,对象的原型又指向对象的父,而父的原型又指向父的父,通过这种原型层层连接起来的关系称为原型链。一个对象的实例作为另一个对象的原型。
函数都有一个prototype属性,指向了这个函数的原型对象,这个对象包含这个函数创建的实例的共享属性和方法,即原型对象的属性和方法为所有实例所共享。这个原型对象有一个constructor属性,指向了该构造函数。每个通过该构造函数创建的对象都包含一个指向原型对象的内部指针_proto_
JavaScript对象由构造函数创建,每个对象都有一个constructor属性,表示该对象的构造函数。所有实例的原型引用的是构造函数的原型属性。
6— 行为委托
。。。。。。