this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。

根据不同的使用场合,this 有不同的值,主要分为下面几种情况:

1. 默认绑定:

什么情况下使用默认绑定呢?独立函数调用。

  • 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

案例一:普通函数调用

  • 该函数直接被调用,并没有进行任何的对象关联;
  • 这种独立的函数调用会使用默认绑定,通常默认绑定时,函数中的 this 指向全局对象(window);
js
function foo() { console.log(this); // window } foo();

案例二:函数调用链(一个函数又调用另外一个函数)

  • 所有的函数调用都没有被绑定到某个对象上;
js
// 2.案例二: function test1() { console.log(this); // window test2(); } function test2() { console.log(this); // window test3() } function test3() { console.log(this); // window } test1();

案例三:将函数作为参数,传入到另一个函数中

js
function foo(func) { func() } function bar() { console.log(this); // window } foo(bar);

我们对案例进行一些修改,考虑一下打印结果是否会发生变化:

  • 这里的结果依然是 window,为什么呢?
  • 原因非常简单,在真正函数调用的位置,并没有进行任何的对象绑定,只是一个独立函数的调用;
js
function foo(func) { func() } var obj = { name: "why", bar: function() { console.log(this); // window } } foo(obj.bar);

2. 隐式绑定:

另外一种比较常见的调用方式是通过某个对象进行调用的:

  • 也就是它的调用位置中,是通过某个对象发起的函数调用。

案例一:通过对象调用函数

  • foo 的调用位置是 obj.foo() 方式进行调用的
  • 那么 foo 调用时 this 会隐式的被绑定到 obj 对象上
js
function foo() { console.log(this); // obj对象 } var obj = { name: "why", foo: foo } obj.foo();

案例二:案例一的变化

  • 我们通过 obj2 又引用了 obj1 对象,再通过 obj1 对象调用 foo 函数;
  • 那么 foo 调用的位置上其实还是 obj1 被绑定了 this
js
function foo() { console.log(this); // obj1 对象 } var obj1 = { name: "obj1", foo: foo } var obj2 = { name: "obj2", obj1: obj1 } obj2.obj1.foo();

案例三:隐式丢失

结果最终是 window,为什么是 window 呢?

  • 因为 foo 最终被调用的位置是 bar ,而 bar 在进行调用时没有绑定任何的对象,也就没有形成隐式绑定;
  • 相当于是一种默认绑定;
js
function foo() { console.log(this); } var obj1 = { name: "obj1", foo: foo } // 讲obj1的foo赋值给bar var bar = obj1.foo; bar();

3. 显示绑定

隐式绑定有一个前提条件:

  • 必须在调用的 对象内部 有一个对函数的引用(比如一个属性);
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
  • 正是通过这个引用,间接的将this绑定到了这个对象上;

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

  • JavaScript 所有的函数都可以使用 callapply 方法(这个和 Prototype 有关)。
    • 它们两个的区别这里不再展开;
    • 其实非常简单,第一个参数是相同的,后面的参数,apply 为数组,call 为参数列表;
  • 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给 this 准备的。
  • 在调用这个函数时,会将 this 绑定到这个传入的对象上。

因为上面的过程,我们明确的绑定了 this 指向的对象,所以称之为 显示绑定

案例一callapply

通过 call 或者 apply 绑定 this 对象

  • 显示绑定后,this 就会明确的指向绑定的对象
js
function foo() { console.log(this); } foo.call(window); // window foo.call({name: "why"}); // {name: "why"} foo.call(123); // Number对象,存放 123

案例二bind 函数

如果我们希望一个函数总是显示的绑定到一个对象上,我们可以使用 bind 函数:

function foo() { console.log(this); } var obj = { name: "why" } var bar = foo.bind(obj); bar(); // obj对象 bar(); // obj对象 bar(); // obj对象

案例三:内置函数

有些时候,我们会调用一些 JavaScript 的内置函数,或者一些第三方库中的内置函数。

  • 这些内置函数会要求我们传入另外一个函数;
  • 我们自己并不会显示的调用这些函数,而且 JavaScript 内部或者第三方库内部会帮助我们执行;
  • 这些函数中的 this 又是如何绑定的呢?
js
// 1. setTimeout中会传入一个函数,这个函数中的this通常是window setTimeout(function() { console.log(this); // window }, 1000); // 2. forEach map filter 等高阶函数 this 通常指向 window对象,但我们可以通过第二个参数改变 var names = ["abc", "cba", "nba"]; names.forEach(function(item) { console.log(this); // 三次window }); var names = ["abc", "cba", "nba"]; var obj = {name: "why"}; names.forEach(function(item) { console.log(this); // 三次obj对象 }, obj);

4. new 绑定

JavaScript 中的函数可以当做一个类的构造函数来使用,也就是使用 new 关键字。

使用 new 关键字来调用函数时,会执行如下的操作:

  1. 创建一个全新的对象;
  2. 这个新对象会被执行 Prototype 连接;
  3. 这个新对象会绑定到函数调用的 thisthis 的绑定在这个步骤完成)
  4. 如果函数没有返回其他对象,表达式会返回这个新对象;
js
// 创建Person function Person(name) { console.log(this); // Person {} this.name = name; // Person {name: "why"} } var p = new Person("why"); console.log(p);

5. 优先级

new绑定 > 显示绑定(bind) > 隐式绑定 > 默认绑定

PS: new 绑定后可以使用 bind 但是 bind 不会生效。 new 绑定后使用 callapply 会报错。

6. 规则之外

  • bind 绑定一个 null 或者 undefined 无效
  • 间接函数引用
js
function foo() { console.log(this); } var obj1 = { name: "obj1", foo: foo }; var obj2 = { name: "obj2" } obj1.foo(); // obj1对象 // 赋值(obj2.foo = obj1.foo)的结果是foo函数 // foo函数被直接调用,那么是默认绑定; (obj2.foo = obj1.foo)(); // window
  • ES6 箭头函数:箭头函数不使用 this 的四种标准规则(也就是不绑定 this ),而是根据外层作用域来决定 this

ES6 中的 class 类 可以通过 extends 关键字实现继承,而这道题,面试官可能更想问的是在 ES5 中如何实现

1. 通过原型链实现继承

javascript
// 父类: 公共属性和方法 function Person() { this.name = "yjw" } // 父类定义一个吃的方法 Person.prototype.eating = function() { console.log(this.name + ' is eating') } // 子类: 特有属性和方法 function Student() { this.sno = '001' } // Student的原型对象指向一个Person的实例对象per const per = new Person() Student.prototype = per // 子类定义一个学习的方法 Student.prototype.studying = function() { console.log(this.name + ' is studying') } const stu = new Student() console.log(stu) console.log(stu.name) // stu对象中没有name属性 会去他的原型对象per上找 per对象上有name属性 stu.eating() // stu对象中没有eating方法 会去他的原型对象per上找per对象上也没eating方法 再往上去per的原型对象上找 per的原型对象上有eating方法 stu.studying()

这种方式总结就是:子类的原型指向父类的一个实例对象

  • 原型链实现继承的弊端
  1. 通过直接打印对象是看不到这个属性的(继承属性看不到
  2. 属性被多个对象共享,如果这个属性是一个引用类型,会造成问题 (修改引用类型 会互相影响
  3. 不能给 Person 传递参数,因为这个对象是一次性创建的(没办法定制化

2. 借用构造函数继承

javascript
// 父类: 公共属性和方法 function Person(name) { this.name = name } // 父类定义一个吃的方法 Person.prototype.eating = function() { console.log(this.name + ' is eating') } // 子类: 特有属性和方法 function Student(name, sno) { // 借用了父类的构造函数 Person.call(this, name) this.sno = sno } // Student的原型对象指向一个Person的实例对象per const per = new Person() Student.prototype = per // 子类定义一个学习的方法 Student.prototype.studying = function() { console.log(this.name + ' is studying') }

借用构造函数继承解决了上面的三个问题。但还是不够完美

  • 存在的问题
  1. 会调用两次父类的构造函数
  2. 子类对象的原型对象上会多出没用的属性

3. 寄生组合式继承

javascript
// 父类: 公共属性和方法 function Person(name) { this.name = name } // 父类定义一个吃的方法 Person.prototype.eating = function() { console.log(this.name + ' is eating') } // 子类: 特有属性和方法 function Student(name, sno) { // 借用了父类的构造函数 Person.call(this, name) this.sno = sno } Student.prototype = Object.create(Person.prototype) // 原型式继承 不用new Person()多调用父类构造函数了 Object.defineProperty(Student.prototype, "constructor", { enumerable: false, configurable: true, writable: true, value: Student }) // 改构造函数名称 // 子类定义一个学习的方法 Student.prototype.studying = function() { console.log(this.name + ' is studying') }
javascript
// 上面的 Object.create(Person.prototype) 也可以写成 (兼容性) Object.setPrototypeOf(Student.prototype, Person.prototype) // 也可以写成 (兼容性) function object(o) { function F() {} F.prototype = o return new F() } Student.prototype = object(Person.prototype)
  • 注意:不要让子类型的原型对象 = 父类型的原型对象,因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改

1. instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

如果让你实现一个 instanceof 应该就很简单了吧?(循环遍历对象的隐式原型直到为 null 或者为 Array

js
let arr = [1, 2]; arr instanceof Array // true

2. 借助 Object 的 toString 方法

js
let arr = [1, 2]; Object.prototype.toString.call(arr) === '[object Array]'

3. constructor

js
let arr = [1,2]; arr.constructor === Array; // true

4. Array.isArray

js
let arr = [1,2]; Array.isArray(arr) // true

在 JS 中,原型和原型链是一个很重要的概念,可以说原型本质就是一个对象。它分为两种: 对象的原型函数的原型

对象的原型:

  • 任何对象都有自己默认的原型(隐式原型),
  • 它的作用就是在当前对象查找某一个属性时, 如果找不到, 会在原型上面查找
  • 获取隐式原型的方法:
    • __proto__(这个不是规范,是浏览器加的,因为早期没有获取原型对象的方法)
    • Object.getPrototypeOf(obj)

函数的原型:

  • 首先,因为函数也是一个对象,所以他会有一个 __proto__ 隐示原型
  • 其次任何一个函数(非箭头函数)还会有自己的 prototype 属性(显式原型
  • 获取显示原型的方法:
    • prototype
  • 作用:
    • 当通过 new 操作符调用函数时, 创建一个新的对象
    • 这个新的对象的 隐式原型 会指向这个函数的 显式原型
    • obj.__proto__ = F.prototype

原型链:

上面讲过,隐式原型的作用是 当前对象查找某一个属性时, 如果找不到, 会在原型上面查找,而原型也是一个对象,所以在原型对象上找不到的话,还会去原型对象的原型对象上找,这样一层一层、以此类推就形成了原型链(prototype chain

1. 第一道

js
var a = {"x": 1}; var b = a; a.x = 2; console.log(b.x); a = {"x": 3}; console.log(b.x); a.x = 4; console.log(b.x);

答案放在了下面,思考一下哦~

原理:

  1. 首先:第一个 log 打印 2 大家应该都没问题 image.png

  2. 然后 a 指向了栈内存中的另一块地址,而 b 没变,所以 b.x 仍然为 2

image.png

  1. 接着修改 a.x = 4 ,因为此时 b 仍然指向之前的地址,所以修改 a.x 并不会去影响 b,所以打印仍然为 2

image.png

答案:2 2 2

2. 第二道:

js
var a = {n:1}; var b = a; a.x = a = {n:2}; console.log(a.x); console.log(b.x);

答案在下面思考下哦~

这次我尝试一张图解释:

image.png

答案: undefined {n: 2}