关于this对象
this对象是在运行时基于函数的执行环境绑定的;全局函数中,this等于window,而当函数的执行环境具有全局性,this对象通常指向window。
var name = "The window";
var object = {
name:"My Object",
getNameFun:function(){
return function(){
return this.name;
}
}
}
alert(object.getNameFun());//"The Window"(非严格模式下)
为什么不是"My Objdect"?
每个函数在被调用时都会自动取得两个特殊变量:this和arguments内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了
var name = "The window";
var object = {
name:"My Object",
getNameFun:function(){
return function(){
var that = this;
return that.name;//"My Object"
}
}
}
特殊情况下,this值可能会改变
var name = "The window";
var object = {
name:"My Object",
getNameFun:function(){
return function(){
return this.name;
}
}
}
①object.getName();//"My Objedt"
②(object.getName())();//"My Object"
③(object.getName = object.getName)();//"The window"非严格模式下
③先执行一条赋值语句然后再调用赋值后的结果,这个赋值表达式的值是函数本身,所以this的值不能维持,"The window"
闭包
闭包是指有权访问另一个函数作用域中的变量的函数
常见的创建闭包的方法:在一个函数内部创建另一个函数
当某个函数被调用时,会创建一个一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。
function compare(value1,value2){
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
-
后台的每个执行环境都有一个表示变量的对象 —— 变量对象
-
全局环境的变量对象始终存在,而像compare()函数这样的局部环境变量的对象,则只在函数执行的过程存在。
-
作用域链本质上是一个指向变量对象的指针列表,只是引用但不实际包含变量对象
-
函数中当我一个变量时,就会从作用域链中搜索具有相应名字的变量。
-
当函数执行完毕后,一般,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象),但闭包情况不同。
-
在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。
这样外部函数执行完毕之后,其活动对象也不会被销毁,因为内部函数的作用域链仍然在引用这个活动对象。外部函数的作用域链会被销毁,但活动对象仍会留在内存中;直至内部函数被销毁后,外部函数的活动对象才会被销毁。//创建函数 var compareNamed = createCompareFunction("name"); //调用函数 var result = compareNames({name:"ABC"},{name:"DRF"}); //解除对匿名函数的引用(以便释放内存) compareNames = null;
创建的比较函数被保存在变量compareNames中,通过将compareNames设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除,随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁。
闭包只能取得包含函数中任何变量的最后一个值
function createFunction(){
var result = new Array();
for(var i=0;i<10;i++){
result[i] = function(){
return i;
}
}
return result;
}
返回一个数组,但每个函数都返回10,。因为每个函数作用域中都保存着createFunction()函数的活动对象,引用都是同一个变量i,每个函数都引用这个保存变量的同一个变量对象,所以函数内容i都是10
通过创建另一个匿名函数强制让闭包的行为符合预期
function createFunction(){
var result = new Array();
for(var i=0;i<10;i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。在调用每个匿名函数时,我们传入变量i。
由于函数参数是按值传递的,所以就会将变量i的当前复制给参数num。而这个匿名函数内部,又创建并返回了一个访问num的闭包。result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值。
内存泄漏
如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
}
}
匿名函数保存了assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占的内存就永远不会被回收
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id
element.onclick = function(){
alert(id);
};
element = null;
}
element.id的一个副本保存在一个变量中,并在闭包中引用该变量消除了循环引用,闭包会引用包含函数的整个活动对象,而其中包含element。
即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。有必要把element变量设置为null,这样就能解除对DOM对象的引用顺利地减少,确保正常回收其占用的内存。
私有变量
JS中没有私有成员的概念;任何在函数中定义的变量,都可以认为是私有变量,因此不能再函数的外部访问这些变量,私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。
来源于《JavaScript高级程序设计》
为什么要用this
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
指向自身
-
有一种传统的但是现在已经被启用和批判的用法,使用arguments.callee来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。现在已被弃用,不该再使用他。
-
记录一下函数foo被调用的次数
function foo(num){ console.log("foo:" + num); //记录foo被调用的次数 foo.count++; } foo.count = 0; var l; for(i=0;i<10;i++){ if(i>5){ foo(i);} } //foo:6,foo:7,foo:8.foo:9 //foo被调用了多少次? console.log(foo.count);//4
这种方法是使用foo标识符替代this来引用函数对象,这种方法同样回避了this的问题,并且完全依赖于foo的词法作用域。
3. 另一种方法是强制this指向函数对象
function foo(num){
console.log("foo:" + num);
//记录foo被调用的次数
//注意,在当前的调用方式下,this确实指向foo
this.count++;
}
foo.count = 0;
var l;
for(i=0;i<10;i++){
if(i>5){
//使用call(..)可以确保this指向函数对象foo本身
foo.call(foo,i);
}
}
//foo:6,foo:7,foo:8.foo:9
//foo被调用了多少次?
console.log(foo.count);//4
this到底是什么
- this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
- 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会被包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的而一个属性,会在执行函数执行的过程中用到。
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的调用位置
默认绑定
1.最常用的函数调用类型:独立函数调用。可以理解为是无法应用其他规则时的默认规则
function fpp(){
console.log(this.a);
}
var a = 2;
foo();//2
- 声明在全局作用域中的变量(比如 var a = 2)就是全局对象的一个同名属性。本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
- 当调用foo()时,this.a被解析成了全局变量a。函数调用时应用了this的默认绑定,因此this指向全局对象。
- 在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
- 虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象,在严格模式下调用foo()则不影响默认绑定。
隐式绑定
-
另一条需要考虑的规则时调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo } obj.foo();
-
首先需要注意的是foo()的声明方式,及其之后是如何被当作属性添加到obj中的,但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。
-
然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。
-
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
隐式丢失
-
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo } var bar = obj.foo;//函数别名 var a = "oops,global";//a是全局对象的属性 bar();//oops,global
-
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo的函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
-
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。
-
如果把函数传入语言内置的函数而不是传入你自己声明的函数,结果还是变得。
-
回调函数丢失this绑定是非常常见的。调用回调函数的函数可能会修改this。
显示绑定
-
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。
-
javascript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(…)和apply(…)方法。
-
它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。
function foo(){ console.log(this.a); } var obj = { a:2; } foo.call(obj);//2
-
通过foo.call(…),我们可以在调用foo时强制把它的this绑定到obj上。
-
如果你传入了一个原始值(字符串类型,布尔类型后者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(…),new Boolean(…)或者new Number(…))。这通常被称为“装箱”。
硬绑定
function foo(){
console.log(this.a);
}
var obj = {
a:2;
}
var bar = function(){
foo.call(obj);
}
bar();//2
setTimeout(bar,100);//2
//硬绑定的bar不可能再修改它的this
bar.call(window);//2
-
我们创建了函数bar(),并在它的内部手动调用了foo.call(obj)。因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显示的强制绑定,因此我们称为硬绑定。
-
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值
function foo(something){ console.log(this.a,something); return this.a + something; } var obj = { a:2 }; var bar = function(){ return foo.apply(obj,arguments); }; var b = bar(3);//2,3 console.log(b);//5
-
另一种使用方法是创建一个可以重复使用的辅助函数
function foo(something){ console.log(this.a,something); return this.a + something; } //简单地辅助绑定函数 function bind(fn,obj){ return function(){ return fn.apply(obj,arguments); }; } var obj = { a:2 }; var bar = bind(foo,obj); var b = bar(3);//2,3 console.log(b);//5
-
ES5提供了内置的方法function.ptototype.bind,它的用法如下:
function foo(something){ console.log(this.a,something); return this.a + something; } var obj = { a:2 }; var bar = bind(obj); var b = bar(3);//2,3 console.log(b);//5
bind(…)会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文并调用原始函数。
API调用的“上下文”
-
第三方库的许多函数,以及javascript语言和宿主环境中许多新的内置函数,都提供了个可选的参数,通常被称为“上下文”,其作用域和bind(…)一样,确保你的回调函数使用指定的this。例:
function foo(el){ console.log(el,this.id); } var obj = { id:"awesome" }; //调用foo(..)时把this绑定到obj [1,2,3].forEach(foo,obj); //1 awesome 2 awesome 3 awesome
new绑定
- 重新定义JavaScript中的“构造函数”,在javascript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
- 包括内置对象函数(比如Number(…))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
- 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
判断this
1.我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。
- 函数是否在new中调用(new绑定)?如果是的话this绑定的是新的创建的对象。var bar = new foo();
- 函数是否通过call,apply(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。var bar = foo.call(obj2);
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。var bar = foo();
绑定例外
间接引用
function foo(){
console.log(this.a);
}
var a = 2;
var o = {a:3,foo:foo};
var p = {a:4};
o.foo();//3
(p.foo = o.foo)();//2
- 赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。这里会应用默认绑定。
- 注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会绑定到全局对象。
软绑定
-
如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
-
软绑定例:
if(!Function.prototype.doftBind){ Function.prototype.softBind = function(obj){ var fn = this; //捕获所有curried参数 var curried = [].slice.call(arguments,1); var bound = function(){ return fn.apply( (!this || this === (window || global))? obj:this, curried.concat.apply(curried,arguments) ); } boun.prototype = Object.create(fn,prototype); return bound; }; }
-
除了软绑定之外,softBind(…)的其他原理和ES5内置的bind(…)类似。它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。此外,这段代码还支持可选的柯里化。
更安全的this
一定要注意:有些调用可能在无意之中使用默认绑定规则。如果想“更安全”地忽略this绑定,你可以使用一个DMZ对象,比如 空符号 = Object.create(null),以保护全局对象。
this词法
-
肩头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。
-
ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前的代码中的self=this机制一样。
-
箭头函数最常用于回调函数中,例如时间处理器或者定时器:
function foo(){ setTimeout(() => { //这里的this在此法上继承来自foo() console.log(this.a); },100); } var obj = { a:2; } foo.call(obj);//2
-
如果你经常编写this风格的代码,但是绝大部分都会使用self = this或者箭头函数来否定this机制,那你或许应当:
- 只使用词法作用域并完全抛弃错误this风格的代码
- 完全采用this风格,在必要时使用bind(…),尽量避免使用self = this和箭头函数。
作用域闭包
实质问题
-
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
-
清晰展示一下闭包:
function foo(){ var a= 2; function bar(){ console.log(a); } return bar; } var baz = foo(); baz();//2
函数bar()的词法作用域能够访问foo()的内部作用域。然后将bar()函数本身当作一个值类型进行传递。在foo()执行后,其返回值(也就是每部的bar()函数)赋值给变量baz并调用baz()。实际上只是通过不同的标识符引用调用了内部的函数bar()。bar()显然可以被执行。但是,它在自己定义的词法作用域以外的地方执行。
3. 在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
4. 而闭包可以阻止这件事情发生。事实上内部作用域依然存在。因此没有被回收。拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得作用域能够一直存活,以供bar()在之后任何时间进行引用。bar()依然持有对该作用于的引用,而这个引用就叫做闭包。
5. 函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
6. 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
7. 只要使用了回调函数,实际上就是使用闭包。
循环和闭包
for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}
我们对这段代码代码行为的预期是分别输出1~5,每一秒一次,一次一个。但是实际上输出5次6.
-
首先解释6是从哪来的,这个循环的终止条件是不再<=5,条件首次成立时i的值是6,因此,输出显示的是循环结束时i的最终值。延迟函数的回调会在循环结束时才执行。事实上当定时器运行时即使每个迭代中执行的是setTimeout(…0),所有的回调函数依然是再循环结束后才被执行,因此会每次输出一个6来。
-
这里缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。
for(var i=1;i<=5;i++){ (function(j){ setTimeout(function timer(){ console.log(j); },j*1000); })(i); }
-
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值得变量供我们访问。
重返块作用域
-
我们使用IIFE在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域。let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。本质上这是将一个块转换成一个可以被关闭的作用域。
for(let i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i*1000); }
模块
function CoolModule(){
var something = "cool";
var another = [1,2,3];
function dosomething(){
consloe.log(something);
}
function doAnother(){
consloe.log(another.join(" ! ");
}
return {
dosomething:dosomething;
doAnother:doAnother;
};
}
var foo = CoolModule;
foo.dosomething();//cool
foo.doAnother();//1 ! 2 ! 3
-
这个模式在javascript中被称为模块。最常见的现实模块模式的方法通常被称为模块暴露,这里展现的是其变体。
-
模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
-
一个具备函数属性的对象本身并不是真正的模块。一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
-
模块模式另一个简单但很强大的用法是命名将要作为公共API返回的对象。
var foo = (function CoolModule(id){ function cahnge(){ //修改公共API publicAPI.identify = identfy2; } function identify1(){ consloe.log(id); } function identify2(){ consloe.log(id.toUpperCase()); } var publicAPI = { cahnge:cahnge; identify:identify1; }; return publicAPI; })("foo module"); foo.identify();//foo module foo.change(); foo.identify();//FOO MODULE
-
通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。
来源于《你不知道的JavaScript》