在理解继承之前,需要知道 js 的三个东西:
- 什么是 JS 原型链
- this 的值到底是什么
- JS 的 new 到底是干什么的
1. 什么是 JS 原型链?
我们知道 JS 有对象,比如
var obj = { name: "obj" };
我们通过控制台把 obj 打印出来:
我们会发现 obj 已经有几个属性(方法)了。那么问题来了:valueOf / toString / constructor 是怎么来?我们并没有给 obj.valueOf 赋值呀。
上面这个图有点难懂,我手画一个示意图:
我们发现控制台打出来的结果是:
- obj 本身有一个属性 name (这是我们给它加的)
- obj 还有一个属性叫做 proto(它是一个对象)
- obj 还有一个属性,包括 valueOf, toString, constructor 等
- obj.proto其实也有一个叫做proto的属性(console.log 没有显示),值为 null
现在回到我们的问题:obj 为什么会拥有 valueOf / toString / constructor 这几个属性?
答案: 这跟 proto有关 。
当我们「读取」 obj.toString 时,JS 引擎会做下面的事情:
- 看看 obj 对象本身有没有 toString 属性。没有就走到下一步。
- 看看 obj.proto 对象有没有 toString 属性, 发现 obj.proto 有 toString 属性, 于是找到了,所以 obj.toString 实际就是第 2 步中找到的 obj.proto.toString。
- 如果 obj.proto没有,那么浏览器会继续查看 obj.proto.proto
- 如果 obj.proto.proto也没有,那么浏览器会继续查看 obj.proto.proto.proto
- 直到找到 toString 或者 proto 为 null。
上面的过程,就是「读」属性的「搜索过程」。而这个「搜索过程」,是连着由 proto 组成的链子一直走的。这个链子,就叫做「原型链」。
共享原型链
现在我们还有另一个对象
var obj2 = { name: "obj2" };
如图:
那么 obj.toString 和 obj2.toString 其实是同一东西, 也就是 obj2.proto.toString。
说白了,我们改其中的一个 proto.toString ,那么另外一个其实也会变!
差异化
如果我们想让 obj.toString 和 obj2.toString 的行为不同怎么做呢?
直接赋值就好了:
obj.toString = function() {
return "新的 toString 方法";
};
小结
- [读]属性时会沿着原型链搜索
- [新增]属性时不会去看原型链
2. this 的值到底是什么
你可能遇到过这样的 JS 面试题:
var obj = {
foo: function() {
console.log(this);
}
};
var bar = obj.foo;
obj.foo(); // 打印出的 this 是 obj
bar(); // 打印出的 this 是 window
请解释最后两行函数的值为什么不一样。
函数调用
JS(ES5)里面有三种函数调用形式:
func(p1, p2);
obj.child.method(p1, p2);
func.call(context, p1, p2); // 先不讲 apply
一般,初学者都知道前两种形式,而且认为前两种形式「优于」第三种形式。
我们方方老师大姥说了,你一定要记住,第三种调用形式,才是正常调用形式:
func.call(context, p1, p2);
其他两种都是语法糖,可以等价地变为 call 形式:
func(p1, p2)等价于 func.call(undefined, p1, p2);
obj.child.method(p1, p2) 等价于 obj.child.method.call(obj.child, p1, p2);
至此我们的函数调用只有一种形式:
func.call(context, p1, p2);
这样,this 就好解释了 this 就是上面 context。
this 是你 call 一个函数时传的 context,由于你从来不用 call 形式的函数调用,所以你一直不知道。
先看 func(p1, p2) 中的 this 如何确定:
当你写下面代码时;
function func() {
console.log(this);
}
func();
等价于;
function func() {
console.log(this);
}
func.call(undefined); // 可以简写为 func.call()
按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则:
如果你传的 context 就 null 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)
因此上面的打印结果是 window。如果你希望这里的 this 不是 window,很简单:
func.call(obj); // 那么里面的 this 就是 obj 对象了
回到题目:
var obj = {
foo: function() {
console.log(this);
}
};
var bar = obj.foo;
obj.foo(); // 转换为 obj.foo.call(obj),this 就是 obj
bar();
// 转换为 bar.call()
// 由于没有传 context
// 所以 this 就是 undefined
// 最后浏览器给你一个默认的 this —— window 对象
[ ] 语法
function fn() {
console.log(this);
}
var arr = [fn, fn2];
arr[0](); // 这里面的 this 又是什么呢?
我们可以把 arr0 想象为 arr.0( ),虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1, p2) 对应上了,于是就可以愉快的转换了:
arr[0]();
假想为 arr.0()
然后转换为 arr.0.call(arr)
那么里面的 this 就是 arr 了