《你不知道的JavaScript》小结1

var obj = {
  id: "awesome",
  cool: function coolFn() {
     console.log( this.id );
  }
};
var id = "not awesome";
obj.cool(); // awesome
setTimeout( obj.cool, 100 ); // not awesome
  1. 关于闭包
for (var i=1; i<=5; i++) {
  (function() {
    setTimeout( function timer() {
      console.log( i );
    }, i*1000 );
   })();
} // 输出全是6
for (var i=1; i<=5; i++) {
  (function() {
    var j = i;
    setTimeout( function timer() {
      console.log( j );
    }, j*1000 );
  })();
}
  1. 关于this
function foo(num) {
  console.log( "foo: " + num );
  // 记录 foo 被调用的次数
  this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
  if (i > 5) {
    foo( i );
  }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0 -- 什么?!

在执行 foo.count=0时,的确想函数对象foo添加了一个属性,但是函数内部代码this.countthis并不是执行那个函数对象,虽然属性名相同,跟对象却并不相同,如果此时打印this.count它为NaN。

结果.png

这里我们可以看到,需要从函数对象内部引用它自身,只使用this是不够的,一般来说,需通过一个指向函数对象的词法标识符(变量)来引用它。
因此,此时我们可以通过强制this指向foo函数对象来解决这个问题:

function foo(num) {
  console.log( "foo: " + num );
  // 记录 foo 被调用的次数
  // 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
  this.count++;
}
foo.count = 0;
var i;
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

在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript代码访问,它存在于 JavaScript 引擎内部。

function foo() {
  var a = 2;
  this.bar();
}

function bar() {
  console.log( this.a );
}

foo(); // ReferenceError: a is not defined

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。foo在外部被调用,此时this指向windowbarwindow环境下,但是window中没有变量a,所以报错。

  • 关于 this运行机制
    this运行时绑定,不是编写时绑定,它的上下文取决于函数调用时的各种条件,this的绑定方式和函数声明位置没有关系,只取决于函数调用方式,当一个函数被调用时会创建一个活动记录(执行上下文),包含函数在哪里被调用、调用方式、传入的参数信息等。this就是这个记录的一个属性,会在函数执行的过程中用到。
    绑定方式:
    I. 默认绑定
function foo() {
   console.log( this.a );
}
var a = 2;
foo(); // 2

这里需要注意只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;则不能将全局对象用于默认绑定,因此 this 会绑定到undefined :

function foo() {
  "use strict";
  console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只
有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;在严格模式下调用
foo() 则不影响默认绑定:

function foo() {
  console.log( this.a );
}
var a = 2;

(function(){
  "use strict";
  foo(); // 2
})();

II. 隐式绑定

function foo() {
  console.log( this.a );
}

var obj2 = {
  a: 42,
  foo: foo
};

var obj1 = {
  a: 2,
  obj2: obj2
};

obj1.obj2.foo(); // 42

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

function foo() {
  console.log( this.a );
}

function doFoo(fn) {
  // fn 其实引用的是 foo
  fn(); // <-- 调用位置!
}

var obj = {
  a: 2,
  foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

相当于

function doFoo() {
  console.log( this.a );
}

var a = "oops, global"; // a 是全局对象的属性
doFoo( ); // "oops, global"

回调函数丢失 this 绑定是非常常见的。除此之外,还有一种情况 this 的行为会出乎我们意料:调用回调函数的函数可能会修改 this 。在一些流行的JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。
III. 显式绑定
可以使用函数的 call(..) 和apply(..) 方法:它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this 。如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..) 、 new Boolean(..) 或者new Number(..) )。这通常被称为“装箱”。

function foo() {
  console.log( this.a );
}
var obj = {
  a:2
};
foo.call( obj ); // 2
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

VI. new 绑定
想回忆一下使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
a. 创建(或者说构造)一个全新的对象。
b. 这个新对象会被执行 [[Prototype]] 连接。
c. 这个新对象会绑定到函数调用的 this 。
d. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

function foo(a) {
  this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

V. 绑定例外
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、 apply 或者 bind ,这些值
在调用时会被忽略,实际应用的是默认绑定规则:

function foo() {
  console.log( this.a );
}

var a = 2;
foo.call( null ); // 2
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

创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。
对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式, this 会被绑定到 undefined ,否则this 会被绑定到全局对象。


判断 this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的
顺序来进行判断:
a. 函数是否在 new中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
b. 函数是否通过 callapply (显式绑定)或者硬绑定调用?如果是的话, this 绑定的是
指定的对象。
var bar = foo.call(obj2)
c. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上
下文对象。
var bar = obj1.foo()
d. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定
到全局对象。
var bar = foo()

  1. 关于箭头函数的this
function foo() {
  // 返回一个箭头函数
  return (a) => {
    //this 继承自 foo()
    console.log( this.a );
  };
}

var obj1 = {
  a:2
};

var obj2 = {
  a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

这里我们看到,箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this 。


对比普通函数

if (!Function.prototype.softBind) {
  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 )
      );
    };
    bound.prototype = Object.create( fn.prototype );
    return bound;
  };
}

function foo() {
  console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj

上面的代码给默认绑定指定一个全局对象和 undefined 以外的值,实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

  1. 关于类型判断
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true

// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354