Skip to content

构造函数与this(Day8 4/4) ​

本页对应 Day08_JavaScript,共 6 段视频,约 50 分钟。

学习目标 ​

  • 能够编写在构造函数中使用 this 为实例添加属性和方法的代码,并说明每次 new 时 this 指向当前创建的实例。
  • 能够根据函数调用形式(如 obj.method()、method()、回调函数调用等)判断 this 的指向,并依据“谁调用指向谁”解释判断过程。
  • 能够独立分析涉及 this、作用域和对象属性访问的题目,写出输出结果并逐步说明理由。
  • 能够区分 this 的动态绑定规则与词法作用域规则,说明分析 this 时应关注调用位置而非函数声明位置。
  • 能够说明全局变量是 window 的属性,并判断访问不存在的变量、不存在的对象属性时分别报错还是得到 undefined。

知识点 ​

转写稿里的“ra/race/瑞子/认死”都是 this,“win度/温州”是 window。本节无图片;转写稿中的代码整理如下。先记住总规则:this 的值是动态的,不看函数声明在哪,只看调用时谁在调用。

先明确:this 不是“指向”,就是当前值 ​

为什么需要:很多同学把“指向”理解成复杂机制,其实初学阶段先把它当成一个变量:它当前的值是谁,this.xxx 就是谁的属性。
怎么想:this 在全局、构造函数、普通函数/方法里取值不同,要分三种情况。
怎么做:全局使用 this,值是 window;构造函数内部,值是 new 出来的实例;函数/方法中,值是调用它的对象。
易错点:不要把 this 和作用域混在一起。作用域看函数创建位置,this 看调用位置,方向相反。

构造函数内部:this 是 new 出来的实例 ​

为什么需要:构造函数要给“将来创建的对象”添加属性,不能写死某个变量名,所以用 this 代表当前实例。
怎么想:new User(...) 执行一次,构造函数体执行一次;这一次的 this 就是这次 new 创建的对象。
怎么做:看下面代码,u1 创建时 this 是 u1,u2 创建时 this 是 u2,互不冲突。
易错点:不写 new,构造函数不会按预期创建实例;构造函数里的局部变量也不会自动变成实例属性,必须写 this.a。

js
function User(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
  this.addToCart = function (product) {
    console.log(this.name + '将' + product + '加入购物车');
  };
}
var u1 = new User('高小乐', 19, '上海');
var u2 = new User('曹操', 89, '北京');
console.log(u1); // this 指向 u1
console.log(u2); // this 指向 u2

函数/方法中:谁调用指向谁 ​

为什么需要:对象方法里经常要访问“当前对象”的属性,this 决定访问的是哪个对象。
怎么想:看调用语句里函数名前面有没有“点什么”。有点,点前对象就是 this;没有点,默认是 window 调用。
怎么做:user.getInfo() 中 this 是 user;user.friend.say() 中 this 是 friend;直接 func() 中 this 是 window。
易错点:把方法赋值给变量后再调用,调用形式变了,this 也会变。

js
var user = {
  name: '高小乐',
  getInfo: function () {
    console.log(this.name);
  },
  friend: {
    name: '曹操',
    say: function () {
      console.log(this.name);
    }
  }
};
user.getInfo();        // 高小乐,this 是 user
user.friend.say();     // 曹操,this 是 friend

function func() {
  console.log(this.name);
}
func();                // this 是 window
var drink = user.getInfo;
drink();               // this 是 window,不是 user

调试推导:同一个函数,换调用方式 this 就变 ​

为什么需要:面试题常把函数传来传去,必须抓住“最终调用点”。
怎么想:不要看函数原来写在哪个对象里,只看最后执行时 fn()、obj.fn()、obj.a.fn() 长什么样。
怎么做:fun() 无点,this 是 window;a.c() 有点,this 是 a;b.say(a.c) 里回调执行的是 fn(),无点,仍是 window。
易错点:看到函数定义在 a 里,就以为 this 永远是 a,这是错的。

js
var name = '222';
var a = {
  name: '111',
  c: function () { console.log(this.name); }
};
var fun = a.c;
fun();      // 222:无点,window 调用
a.c();      // 111:a 调用

var b = {
  name: '333',
  say: function (fn) { fn(); }
};
b.say(a.c); // 222:fn() 无点,window 调用
b.c = a.c;
b.c();      // 333:b 调用

全局变量是 window 的属性 ​

为什么需要:很多题里 this.foo = 234 会改到全局变量,不理解这一点就会算错。
怎么想:全局变量本质上是 window 的属性;this 是 window 时,this.foo 就是 window.foo,也就是全局 foo。
怎么做:下面案例中 print() 无点调用,this 是 window,this.foo = 234 把全局 foo 改成 234。
易错点:使用不存在的变量会报错;使用对象不存在的属性、数组不存在的元素,得到 undefined。window.address 不报错,直接 address 会报错。

js
var foo = 123;
function print() {
  this.foo = 234; // this 是 window,改的是全局 foo
  console.log(foo); // 234
}
print();
console.log(foo); // 234

console.log(window.address); // undefined
// console.log(address);     // ReferenceError

构造函数里的变量作用域与普通函数一样 ​

为什么需要:构造函数里既有局部变量又有实例属性,容易把 a 和 this.a 混为一谈。
怎么想:构造函数本质还是函数,局部变量按作用域规则找;this.a 才是在读新实例的属性。
怎么做:var a = 0 是局部变量;新实例上没有 a 属性,所以 this.a 得到 undefined。
易错点:不要以为构造函数里的 a 会自动挂到新对象上,必须写 this.a。

js
function Test() {
  var a = 0;
  console.log(a);      // 0,本作用域局部变量
  console.log(this.a); // undefined,新实例没有 a 属性
  console.log(a);      // 0
}
new Test();

最后再压缩成一句:构造函数内看 new 出来的实例;普通函数/方法看调用点,点前是谁就是谁,没有点就是 window;this 是动态的,别拿声明位置判断。

先明确:this 到底在问什么 ​

这一节讨论的不是“this 怎么写”,而是“this 的值是谁”。在代码里,this 是一个系统内置的变量,它当然有值;所谓“this 的指向”,本质上就是问 this 当前取值是什么。难点在于:this 的值不是固定不变的,它会随着使用位置和调用方式变化。

初学阶段先记住三条规则:

  1. 在函数外边、全局作用域中使用,this 是 window。
  2. 在构造函数内部使用,this 是本次 new 所创建出来的实例对象。
  3. 在函数或方法中使用,this 是“谁调用这个函数/方法,就是谁”,常概括为“谁调用指向谁”。

其中第三条最难,因为调用点有时不明显。

构造函数中的 this:本次 new 出来的对象 ​

为什么需要它?因为构造函数要给“将来创建出来的对象”添加属性。若没有 this,构造函数内部无法表示“当前正在创建的那个实例”。

怎么想?看 new 的时刻。构造函数体不是写了就执行,而是 new 时才执行;每 new 一次,就执行一次,this 就是那一次创建出来的对象。

怎么做?看下面的例子:

js
function User(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
  this.addToCart = function (product) {
    console.log(this.name + "将" + product + "加入购物车");
  };
}

var u1 = new User("高小乐", 19, "上海");
console.log(u1); // this.name 等属性都加到了 u1 上

这里 this.name = name 就是把形参 name 加到本次创建的对象上。u1 和构造函数里的 this 指向同一个对象。

易错点:如果再 new 一次:

js
var u2 = new User("曹操", 89, "北京");

不要纠结“this 到底是 u1 还是 u2”。创建 u1 时,this 是 u1;创建 u2 时,this 是 u2。两次实例化各执行一次构造函数,互不冲突。

函数/方法中的 this:盯住调用点 ​

为什么难?因为函数声明的位置不等于调用位置。this 看的是“最终谁调用了它”,不是“它写在哪里”。

怎么想?看调用语句里函数名前面有没有“某某点”。有点,点前面的对象就是 this;没有点,就是 window 调用。

js
var user = {
  name: "高小乐",
  age: 189,
  getInfo: function () {
    console.log("getInfo");
    console.log(this);
    console.log(this.name);
  },
  friend: {
    name: "曹操",
    say: function () {
      console.log("say");
      console.log(this.name);
    }
  }
};

user.getInfo();        // this 是 user,输出高小乐
user.friend.say();     // this 是 user.friend,输出曹操

user.friend.say() 中,say 前面是 user.friend,所以 this 是 friend 这个对象。可以用“外婆、妈妈和你”的关系帮助理解:谁直接调用了这个方法,就看谁。

再看全局函数:

js
var name = "龙傲天";
function func() {
  console.log(this);
  console.log(this.name);
}
func(); // 前面没有点,等价于 window.func(),this 是 window

易错点:全局变量本质上是 window 的属性,所以 this.name 能取到全局 name。

this 是动态的:同一个函数,调用方式不同结果不同 ​

这是本节最重要的结论。不要把 this 当成词法作用域那样“看函数声明在哪里”,而要看调用语句。

js
var drink = user.getInfo;
drink(); // 前面没有点,this 是 window

drink 和 user.getInfo 的值是同一个函数,但 user.getInfo() 由 user 调用,drink() 由 window 调用,所以 this 不同。再如:

js
user.friend.eat = func;
user.friend.eat(); // this 是 user.friend

虽然 func 声明在外面,但最终是 user.friend.eat() 调用,点前是 user.friend,所以 this 就是它。

面试题拆解:只找调用点 ​

js
var name = "222";
var a = {
  name: "111",
  say: function () {
    console.log(this.name);
  }
};

var fun = a.say;
fun();       // 222:fun() 前面没有点,this 是 window
a.say();     // 111:a.say() 由 a 调用,this 是 a

高阶函数里也一样:

js
var b = {
  name: "333",
  say: function (fn) {
    fn();
  }
};

b.say(a.say); // 222:fn() 调用时前面没有点,this 是 window
b.say = a.say;
b.say();      // 333:b.say() 由 b 调用,this 是 b

易错点:不要因为函数 originally 写在 a 里,就认为 this 永远是 a。this 看调用点。

结合作用域与 undefined 的题目 ​

js
var foo = 123;
function print() {
  this.foo = 234;
  console.log(foo);
}
print();       // 234
console.log(foo); // 234

print() 无点调用,this 是 window;this.foo = 234 改的是全局变量 foo,因为全局变量就是 window 的属性。

再看:

js
function Test() {
  var a = 0;
  console.log(a);        // 0
  console.log(this.a);   // undefined
  console.log(a);        // 0
}
new Test();

构造函数里 var a 是局部变量,不是实例属性;this.a 读取新对象上不存在的属性,得到 undefined。注意:使用不存在的变量会报错;读取对象不存在的属性、数组不存在的元素,得到 undefined;而 window.address 这种写法是在读 window 的不存在属性,也得到 undefined。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:在构造函数中使用 this ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>构造函数中使用 this</title>
</head>
<body>
<script>
  function User(name, age, address) {
    // 这里的 this 就是 new 出来的那个对象
    this.name = name;
    this.age = age;
    this.address = address;

    this.addShopcart = function (product) {
      console.log(this.name + ' 把 ' + product + ' 加入了购物车');
    };

    console.log('构造函数内部的 this:', this);
  }

  var u1 = new User('高小乐', 19, '上海');
  var u2 = new User('曹操', 89, '北京');

  console.log('外部的 u1:', u1);   // 与构造函数内部打印的 this 是同一个对象
  console.log('外部的 u2:', u2);

  u1.addShopcart('手机');
  u2.addShopcart('电脑');
</script>
</body>
</html>

要点解释

  • this.name = name:构造函数里的 this 指向「本次 new 所创建的对象」,所以属性全部挂到新对象上。
  • 只有在 new User(...) 执行时,构造函数体才会运行;不写 new,里面的代码一行都不执行。
  • 同一个构造函数被实例化多次时 this 不冲突:创建 u1 时 this 是 u1,创建 u2 时 this 是 u2。

易错点

  • 构造函数内部 this 不是 window,而是将来要创建的实例;在全局作用域下 this 才是 window。
  • 想给实例加属性/方法,必须写成 this.xxx = ...;写成 var a = 0 只是局部变量,不会成为实例属性。

示例二:在函数(方法)中使用 this —— 谁调用指向谁 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>在函数中使用 this</title>
</head>
<body>
<script>
  var user = {
    name: '高小乐',
    age: 189,
    getInfo: function () {
      console.log('getInfo');
      console.log(this);
      console.log(this.name);
    },
    friends: {
      name: '曹操',
      say: function () {
        console.log('say');
        console.log(this);
        console.log(this.name);
      }
    }
  };

  // 1. 点前面是 user
  user.getInfo();

  // 2. 点前面是 user.friends(不是 user)
  user.friends.say();

  // 3. 全局变量 name,本质是 window.name
  var name = '龙傲天';

  function func() {
    console.log('func');
    console.log(this);
    console.log(this.name);
  }

  // 4. func 前面没有点,等价于 window.func()
  func();

  // 5. 把方法的值赋给变量,调用时前面没有点
  var drink = user.getInfo;
  drink();          // this 是 window,不是 user

  // 6. 同一个函数,赋给 user.friends.eat 后由 friends 调用
  user.friends.eat = func;
  user.friends.eat();   // this 是 user.friends(曹操)
</script>
</body>
</html>

要点解释

  • 判断规则只有一条:看调用语句里点前面是哪个对象,this 就是谁。
  • user.friends.say() 只看最后一个点前面的对象,即 user.friends,所以 this.name 是「曹操」。
  • func() 前面没有点,实际是 window.func(),所以 this 是 window,this.name 是全局的「龙傲天」。
  • drink = user.getInfo 只是把函数的值取出来,drink() 调用时前面没有点,this 变成 window。

易错点

  • this 是动态的:不要看函数在哪里声明,要看函数在哪里被调用(与作用域规则正好相反)。
  • user.getInfo 与 drink 指向同一个函数,但调用形式不同,this 就不同。
  • 全局变量就是 window 的属性,所以 var name = '龙傲天' 后 window.name 也是它。

示例三:this 面试题综合 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>this 题目</title>
</head>
<body>
<script>
  var name = '222';

  var a = {
    name: '111',
    say: function () {
      console.log(this.name);
    }
  };

  var fun = a.say;
  fun();       // 222:fun() 前面没点,this 是 window
  a.say();     // 111:a.say(),this 是 a

  var b = {
    name: '333',
    say: function (fn) {
      fn();    // 这里调用的是形参 fn,前面没有点
    }
  };

  b.say(a.say);  // 222:fn 被 window 调用
  b.say = a.say;
  b.say();       // 333:b.say(),this 是 b
</script>
</body>
</html>

要点解释

  • var fun = a.say; fun():函数内容没变,但调用时前面没有点,this 是 window,输出全局的 222。
  • b.say(a.say):实参 a.say 赋给形参 fn,等价于 fn = a.say;函数体里是 fn(),前面没有点,仍然是 window 调用,输出 222。
  • b.say = a.say; b.say():此时是 b 在调用,this 是 b,输出 333。

易错点

  • 形参 fn 拿到函数值后,它只是普通变量,不是任何对象的属性,所以调用时 this 一定是 window。
  • 只要调用语句里点前面没有对象,就一律当成 window.xxx() 处理。

常见错误 ​

  1. 现象:把构造函数当普通函数直接调用,this 输出成 window → 原因:没有用 new,调用点前无对象 → 修法:构造函数统一用 new 调用,并检查调用形式。
  2. 现象:看到函数写在某个对象里,就认定 this 一定是该对象 → 原因:按声明位置判断 this → 修法:只看最终调用语句,重点看调用点前的“点”。
  3. 现象:var f = obj.method; f(); 后仍认为 this 是 obj → 原因:函数被赋值给变量后,调用点已变成无点调用 → 修法:分析赋值后的调用点,必要时用 obj.method() 或显式绑定。
  4. 现象:回调函数里 this 丢失,输出了全局值 → 原因:回调以普通函数形式被调用 → 修法:保留调用点、使用绑定,或改用能保留 this 的写法。
  5. 现象:构造函数里写 var a = 9; 却以为实例上有 this.a → 原因:局部变量未挂到实例上 → 修法:需要实例属性时显式写 this.a = ...。
  6. 现象:访问不存在的变量直接报错,访问不存在的属性却得到 undefined,误以为规则矛盾 → 原因:变量用法与属性用法不同 → 修法:区分“变量是否存在”和“对象/数组元素是否存在”,按使用方式判断。
  7. 现象:多次 new 后纠结 this 到底指哪个实例 → 原因:把多次实例化混成一次执行 → 修法:按每次 new 的调用分别分析,本次 new 对应本次实例。

练习 ​

  1. 分析输出:
js
var name = 'global';
var obj = {
  name: 'obj',
  fn: function () {
    console.log(this.name);
  }
};
var f = obj.fn;
f();
obj.fn();
  1. 分析三处输出:
js
var a = 0;
function Test() {
  var a = 9;
  console.log(a);
  console.log(this.a);
  console.log(a);
}
new Test();
  1. 分析输出:
js
var name = 'global';
var A = {
  name: 'A',
  say: function () {
    console.log(this.name);
  }
};
var B = {
  name: 'B',
  say: function (fn) {
    fn();
  }
};
B.say(A.say);
  1. 分析输出:
js
var foo = 123;
function print() {
  console.log(foo);
  this.foo = 234;
  console.log(foo);
}
print();
console.log(foo);
  1. 分析输出:
js
var user = {
  name: '高小乐',
  getInfo: function () {
    console.log(this.name);
  }
};
var friend = {
  name: '曹操',
  say: user.getInfo
};
friend.say();

原视频对照 ​

#视频时长
1在构造函数中使用this08:46
2在函数中使用this16:04
3this题目0110:54
4this题目0203:48
5this的题目0306:52
6全局变量是window属性03:36

小结 ​

  1. 判断 this 先找最终调用点,再看调用点前有没有对象。
  2. 无点调用按 window 处理;有点调用看“点”前是谁。
  3. 构造函数内 this 对应本次 new 创建的实例,多次实例化分别判断。
  4. 函数声明位置不决定 this,赋值、回调、转存都会改变调用点。
  5. 构造函数内局部变量不等于实例属性,实例属性必须显式挂到 this。
  6. 全局变量与 window 属性关系密切,但“变量用法”和“属性用法”对不存在情况的处理不同。
  7. 综合题要同时检查 this、作用域、undefined 与报错规则,不能只看单一知识点。