this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
根据不同的使用场合,this
有不同的值,主要分为下面几种情况:
什么情况下使用默认绑定呢?独立函数调用。
案例一:普通函数调用
this
指向全局对象(window
);jsfunction 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();
案例三:将函数作为参数,传入到另一个函数中
jsfunction foo(func) {
func()
}
function bar() {
console.log(this); // window
}
foo(bar);
我们对案例进行一些修改,考虑一下打印结果是否会发生变化:
window
,为什么呢?jsfunction foo(func) {
func()
}
var obj = {
name: "why",
bar: function() {
console.log(this); // window
}
}
foo(obj.bar);
另外一种比较常见的调用方式是通过某个对象进行调用的:
案例一:通过对象调用函数
foo
的调用位置是 obj.foo()
方式进行调用的foo
调用时 this
会隐式的被绑定到 obj
对象上jsfunction foo() {
console.log(this); // obj对象
}
var obj = {
name: "why",
foo: foo
}
obj.foo();
案例二:案例一的变化
obj2
又引用了 obj1
对象,再通过 obj1
对象调用 foo
函数;foo
调用的位置上其实还是 obj1
被绑定了 this
;jsfunction 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
在进行调用时没有绑定任何的对象,也就没有形成隐式绑定;jsfunction foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo
}
// 讲obj1的foo赋值给bar
var bar = obj1.foo;
bar();
隐式绑定有一个前提条件:
对象内部
有一个对函数的引用(比如一个属性);如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?
JavaScript
所有的函数都可以使用 call
和 apply
方法(这个和 Prototype
有关)。
apply
为数组,call
为参数列表;this
准备的。this
绑定到这个传入的对象上。因为上面的过程,我们明确的绑定了 this
指向的对象,所以称之为 显示绑定。
案例一:call
、apply
通过 call
或者 apply
绑定 this
对象
this
就会明确的指向绑定的对象jsfunction 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);
new
绑定JavaScript
中的函数可以当做一个类的构造函数来使用,也就是使用 new
关键字。
使用 new
关键字来调用函数时,会执行如下的操作:
Prototype
连接;this
上 (this
的绑定在这个步骤完成);js// 创建Person
function Person(name) {
console.log(this); // Person {}
this.name = name; // Person {name: "why"}
}
var p = new Person("why");
console.log(p);
new绑定
> 显示绑定(bind)
> 隐式绑定
> 默认绑定
PS: new
绑定后可以使用 bind
但是 bind
不会生效。 new
绑定后使用 call
和 apply
会报错。
bind
绑定一个 null
或者 undefined
无效jsfunction 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
中如何实现
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()
这种方式总结就是:子类的原型指向父类的一个实例对象
Person
传递参数,因为这个对象是一次性创建的(没办法定制化)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')
}
借用构造函数继承解决了上面的三个问题。但还是不够完美
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
)
jslet arr = [1, 2];
arr instanceof Array // true
2. 借助 Object 的 toString 方法
jslet arr = [1, 2];
Object.prototype.toString.call(arr) === '[object Array]'
3. constructor
jslet arr = [1,2];
arr.constructor === Array; // true
4. Array.isArray
jslet arr = [1,2];
Array.isArray(arr) // true
在 JS 中,原型和原型链是一个很重要的概念,可以说原型本质就是一个对象。它分为两种: 对象的原型和 函数的原型
对象的原型:
__proto__
(这个不是规范,是浏览器加的,因为早期没有获取原型对象的方法)Object.getPrototypeOf(obj)
函数的原型:
__proto__
隐示原型prototype
属性(显式原型)prototype
new
操作符调用函数时, 创建一个新的对象obj.__proto__ = F.prototype
原型链:
上面讲过,隐式原型的作用是 当前对象查找某一个属性时, 如果找不到, 会在原型上面查找,而原型也是一个对象,所以在原型对象上找不到的话,还会去原型对象的原型对象上找,这样一层一层、以此类推就形成了原型链(prototype chain
)
1. 第一道
jsvar 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);
答案放在了下面,思考一下哦~
原理:
首先:第一个 log
打印 2
大家应该都没问题
然后 a
指向了栈内存中的另一块地址,而 b
没变,所以 b.x
仍然为 2
a.x = 4
,因为此时 b
仍然指向之前的地址,所以修改 a.x
并不会去影响 b
,所以打印仍然为 2
答案:2 2 2
2. 第二道:
jsvar a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a.x);
console.log(b.x);
答案在下面思考下哦~
这次我尝试一张图解释:
答案: undefined {n: 2}