JS前端进阶/面试
- 附加
- [前端 100 问:能搞懂80%的请把简历给我](https://github.com/yygmind/blog/issues/43)
- [Daily-Interview-Question 面试题相关](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues)`推荐`
- [vue 如何优化首页的加载速度?vue 首页白屏是什么问题引起的?如何解决呢?](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/234)
- this指向
- 深入之重新认识箭头函数的this
- 变量提升
- 变量的存放(栈、堆)
- `.`和`=`优先级,引用对象问题
- 内存空间管理(标记清除)
- 4类常见内存泄漏和垃圾回收(标记清除)
- 垃圾回收
- 从(内存)来看 null 和 undefined 本质的区别是什么?
- 作用域链
- 闭包
- call/apply
- 实现call 和 apply
- bind
- [new 模拟实现](https://muyiy.cn/blog/3/3.5.html)
- 原型/原型链
- 再探原型链及其继承优缺点
附加
前端 100 问:能搞懂80%的请把简历给我
Daily-Interview-Question 面试题相关推荐
vue 如何优化首页的加载速度?vue 首页白屏是什么问题引起的?如何解决呢?
this指向
我们知道this绑定规则一共有5种情况:
1、默认绑定(严格 undefined/非严格模式 this指向全局对象)
2、隐式绑定 obj.foo()
3、显式绑定 call/apply
4、new绑定
5、箭头函数绑定
其实大部分情况下可以用一句话来概括,this总是指向调用该函数的对象。
思考题
题1:
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3;
(function() {
console.log(this.num);
this.num = 4;
})(); // 这个是立即执行函数, 赋值表达式从右到左, 所以指向全局
console.log(this.num);
},
sub: function() {
console.log(this.num)
}
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
答案有两种情况,分为严格模式和非严格模式。
- 严格模式下,报错。TypeError: Cannot read property ‘num’ of undefined
- 非严格模式下,输出:1、3、3、4、4
解答过程:
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3; // 隐式绑定 修改 myObject.num = 3
(function() {
console.log(this.num); // 默认绑定 输出 1
this.num = 4; // 默认绑定 修改 window.num = 4
})();
console.log(this.num); // 隐式绑定 输出 3
},
sub: function() {
console.log(this.num) // 因为丢失了隐式绑定的myObject,所以使用默认绑定 输出 4
}
}
myObject.add(); // 1 3
console.log(myObject.num); // 3
console.log(num); // 4
var sub = myObject.sub;// 丢失了隐式绑定的myObject
sub(); // 4
题2:
// 1、赋值语句是右执行的,此时会先执行右侧的对象
var obj = {
// 2、say 是立即执行函数
say: function() {
function _say() {
// 5、输出 window
console.log(this);
}
// 3、编译阶段 obj 赋值为 undefined
console.log(obj);
// 4、obj是 undefined,bind 本身是 call实现,
// 【进阶3-3期】:call 接收 undefined 会绑定到 window。
return _say.bind(obj);
}(),
};
obj.say();
深入之重新认识箭头函数的this
箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。
箭头函数的绑定无法被修改
/**
* 非严格模式
*/
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1()
person1.show1.call(person2)
person1.show2() // 注意这个
person1.show2.call(person2) // 注意这个
person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()
person1.show4()() // 注意这个
person1.show4().call(person2) // 注意这个
person1.show4.call(person2)() // 注意这个
正确答案如下:
person1.show1() // person1,隐式绑定,this指向调用者 person1
person1.show1.call(person2) // person2,显式绑定,this指向 person2
person1.show2() // window,箭头函数绑定,this指向外层作用域,即全局作用域
person1.show2.call(person2) // window,箭头函数绑定,this指向外层作用域,即全局作用域
person1.show3()() // window,默认绑定,这是一个高阶函数,调用者是window
// 类似于`var func = person1.show3()` 执行`func()`
person1.show3().call(person2) // person2,显式绑定,this指向 person2
person1.show3.call(person2)() // window,默认绑定,调用者是window
person1.show4()() // person1,箭头函数绑定,this指向外层作用域,即person1函数作用域
person1.show4().call(person2) // person1,箭头函数绑定,
// this指向外层作用域,即person1函数作用域
person1.show4.call(person2)() // person2
变量提升
JS是单线程的语言,执行顺序肯定是顺序执行,但是JS 引擎并不是一行一行地分析和执行程序,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。
函数声明优先级高于变量声明。
需要注意的是同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明。
// 1
foo; // undefined
var foo = function () {
console.log('foo1');
}
// 2
foo(); // foo2
var foo = function() {
console.log('foo1');
}
foo(); // foo1,foo重新赋值
function foo() {
console.log('foo2');
}
foo(); // foo1
变量的存放(栈、堆)
首先我们应该知道内存中有栈和堆,那么变量应该存放在哪里呢,堆?栈?
- 1、基本类型 --> 保存在栈内存中,因为这些类型在内存中分别占有固定大小的空间,通过按值来访问。基本类型一共有6种:Undefined、Null、Boolean、Number 、String和Symbol
- 2、引用类型 --> 保存在堆内存中,因为这种值的大小不固定,因此不能把它们保存到栈内存中,但内存地址大小的固定的,因此保存在堆内存中,在栈内存中存放的只是该对象的访问地址。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。
几个问题
问题1:
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
问题2:
var a = { name: '前端开发' }
var b = a;
b.name = '进阶';
// 这时a.name的值是多少
问题3:
var a = { name: '前端开发' }
var b = a;
a = null;
// 这时b的值是多少
三个问题的答案分别是20、‘进阶’、{ name: '前端开发' }
.
和=
优先级,引用对象问题
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
a.x // --> undefined
b.x // --> {n: 2}
答案已经写上面了,这道题的关键在于
- 1、优先级。
.
的优先级高于=
,所以先执行a.x
,堆内存中的{n: 1}
就会变成{n: 1, x: undefined}
,改变之后相应的b.x
也变化了,因为指向的是同一个对象。 - 2、赋值操作是从右到左,所以先执行
a = {n: 2}
,a
的引用就被改变了,然后这个返回值又赋值给了a.x
,需要注意的是这时候a.x
是第一步中的{n: 1, x: undefined}
那个对象,其实就是b.x
,相当于b.x = {n: 2}
内存空间管理(标记清除)
JavaScript的内存生命周期是
1、分配你所需要的内存
2、使用分配到的内存(读、写)
3、不需要时将其释放、归还
JavaScript有自动垃圾收集机制,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,使用a = null
其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在开发中,需要尽量避免使用全局变量。
4类常见内存泄漏和垃圾回收(标记清除)
垃圾回收
对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用,常用垃圾回收算法有下面两种。
- 引用计数(现代浏览器不再使用),引用计数有一个致命的问题,那就是循环引用
- 标记清除(常用)
标记清除算法将“不再使用的对象”定义为“无法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。
垃圾回收算法
常用垃圾回收算法叫做标记清除 (Mark-and-sweep),算法由以下几步组成:
-
1、垃圾回收器创建了一个“roots”列表。roots 通常是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被当作 root 。window 对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
-
2、所有的 roots 被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从 root 开始的所有对象如果是可达的,它就不被当作垃圾。
-
3、所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收
WeakMap
ES6 新出的两种数据结构:WeakSet 和 WeakMap,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
先新建一个 Weakmap
实例,然后将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap
里面。这时,WeakMap
里面对element
的引用就是弱引用,不会被计入垃圾回收机制。
四种常见的JS内存泄漏
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
1、意外的全局变量
未定义的变量会在全局对象创建一个新变量,如下。
function foo(arg) {
bar = "this is a hidden global variable";
}
函数 foo 内部忘记使用 var ,实际上JS会把bar挂载到全局对象上,意外创建一个全局变量。
function foo(arg) {
window.bar = "this is an explicit global variable";
}
另一个意外的全局变量可能由 this 创建。
function foo() {
this.variable = "potential accidental global";
}
// Foo 调用自己,this 指向了全局对象(window)
// 而不是 undefined
foo();
解决方法:
在 JavaScript 文件头部加上 ‘use strict’,使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
2、被遗忘的计时器或回调函数
计时器setInterval代码很常见
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,interval 仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。
var element = document.getElementById('button');
function onClick(event) {
element.innerHTML = 'text';
}
element.addEventListener('click', onClick);
对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。
但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用 removeEventListener 了
3、脱离 DOM 的引用
如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。那么将来需要把两个引用都清除。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// 更多逻辑
}
function removeButton() {
// 按钮是 body 的后代元素
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
如果代码中保存了表格某一个 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 以外的其它节点。实际情况并非如此:此 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 的引用,导致整个表格仍待在内存中。所以保存 DOM 元素引用的时候,要小心谨慎。
4、闭包
闭包的关键是匿名函数可以访问父级作用域的变量。
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
每次调用 replaceThing
,theThing
得到一个包含一个大数组和一个新闭包(someMethod)
的新对象。同时,变量 unused
是一个引用 originalThing
的闭包(先前的 eplaceThing
又调用了 theThing
)。someMethod
可以通过 theThing
使用,someMethod
与 unused
分享闭包作用域,尽管 unused
从未使用,它引用的 originalThing
迫使它保留在内存中(防止被回收)。
解决方法:
在 replaceThing
的最后添加 originalThing = null
。
PS:今晚弄到很晚,由于时间问题,就不再详细介绍Chrome 内存剖析工具,有兴趣的大家去原文查看
从(内存)来看 null 和 undefined 本质的区别是什么?
Null 只有一个值,是 null。不存在的对象。
Undefined 只有一个值,是undefined。没有初始化。undefined 是从 null 中派生出来的。
简单理解就是:undefined 是没有定义的,null 是定义了但是为空。
解答:
(内存)
给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果是给对象的属性 赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。
给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值 为undefined,说明这个值为空值
作用域链
前:
Javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
详情查看 【进阶1-2期】JavaScript深入之执行上下文栈和变量对象
作用域链定义:
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
复杂的说法
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。
作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError。
作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中
闭包
红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数
关键在于下面两点:
- 是一个函数
- 能访问另外一个函数作用域中的变量
MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。
其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。
使用上一篇文章的例子来说明下自由变量
function getOuter(){
var date = '815';
function getDate(str){
console.log(str + date); //访问外部的date
}
return getDate; //外部函数返回
}
var today = getOuter();
today('今天是:'); //"今天是:815"
today('明天不是:'); //"明天不是:815"
令
today = null
即可回收变量
其中date
既不是参数arguments
,也不是局部变量,所以date
是自由变量。
总结起来就是下面两点:
- 1、是一个函数(比如,内部函数从父函数中返回)
- 2、能访问上级函数作用域中的变量(哪怕上级函数上下文已经销毁)
题1:用 JS 实现一个无限累加的函数 add
示例如下:
add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10
实现:
function add(a) {
function sum(b) { // 使用闭包
a = a + b; // 累加
return sum;
}
sum.toString = function() { // 重写toString()方法
return a;
}
return sum; // 返回一个函数
}
add(1); // 1
add(1)(2); // 3
题2:for循环的问题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
如果知道闭包的,答案就很明显了,都是3
循环结束后,全局执行上下文的VO是
globalContext = {
VO: {
data: [...],
i: 3
}
}
执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
由于其自身没有i变量,就会向上查找,所有从全局上下文查找到i为3,data[1] 和 data[2] 是一样的。
解决办法
改成闭包,方法就是data[i]返回一个函数,并访问变量i
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
循环结束后的全局执行上下文没有变化。
执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO, globalContext.VO]
}
匿名函数执行上下文的AO为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
因为闭包执行上下文中贮存了变量i,所以根据作用域链会在globalContext.VO中查找到变量i,并输出0。
或者改成let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
代码解释原理(闭包):
var data = [];// 创建一个数组data;
// 进入第一次循环
{
let i = 0; // 注意:因为使用let使得for循环为块级作用域
// 此次 let i = 0 在这个块级作用域中,而不是在全局环境中
data[0] = function() {
console.log(i);
};
}
循环时,let
声明i
,所以整个块是块级作用域,那么data[0]这个函数就成了一个闭包。这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。
上面的块级作用域,就像函数作用域一样,函数执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着块级作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。
在上面这个执行环境中,它会首先寻找该执行环境中是否存在i
,没有找到,就沿着作用域链继续向上到了其所在的块作用域执行环境,找到了i = 0
,于是输出了0
。
文字解释:
let
关键字将 for 循环的块隐式地声明为块作用域。而 for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
这是->>书:《你不知道的 JavaScript》中的解释。
babel 转了下发现其实多了_loop的函数
立即执行函数
for (var i = 0; i < 3; i++) {
(function(num) {
setTimeout(function() {
console.log(num);
}, 1000);
})(i);
}
// 0 // 1 // 2
返回一个匿名函数赋值
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function(){
console.log(num);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
无论是立即执行函数还是返回一个匿名函数赋值,原理上都是因为变量的按值传递,所以会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于访问num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。
call/apply
call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。
var func = function(arg1, arg2) {
...
};
func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组
使用场景
1、合并两个数组
var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];
// 将第二个数组融合进第一个数组
// 相当于 vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);
当第二个数组(如示例中的 moreVegs )太大时不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。不同的引擎有不同的限制,JS核心限制在 65535,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。
如何解决呢?方法就是将参数数组切块后循环传入目标方法
function concatOfArray(arr1, arr2) {
var QUANTUM = 32768;
for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
Array.prototype.push.apply(
arr1,
arr2.slice(i, Math.min(i + QUANTUM, len) )
);
}
return arr1;
}
// 验证代码
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
arr2.push(i);
}
Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded
concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
2、获取数组中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ];
Math.max.apply(Math, numbers); //458
// 这块在调用的时候第一个参数给了一个null,这个是因为没有对象去调用这个方法,
// 我只需要用这个方法帮我运算,得到返回的结果就行,所以直接传递了一个null过去
Math.max.apply(null, numbers); //458
Math.max.call(Math, 5, 458 , 120 , -215); //458
// ES6
Math.max.call(Math, ...numbers); // 458
3、判断类型
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
// 直接使用 toString()
[1, 2, 3].toString(); // "1,2,3"
"123".toString(); // "123"
123.toString(); // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"
可以通过toString()
来获取每个对象的类型,但是不同对象的 toString()
有不同的实现,所以通过Object.prototype.toString()
来检测,需要以 call() / apply()
的形式来调用,传递要检查的对象作为第一个参数。
另一个验证是否是数组的方法
var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){
return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
// 使用改造后的 toStr
toStr([1, 2, 3]); // "[object Array]"
toStr("123"); // "[object String]"
toStr(123); // "[object Number]"
toStr(Object(123)); // "[object Number]"
上面方法首先使用 Function.prototype.call
函数指定一个 this
值,然后 .bind
返回一个新的函数,始终将 Object.prototype.toString
设置为传入参数。其实等价于 Object.prototype.toString.call()
。
这里有一个前提是toString()
方法没有被覆盖
4、类数组对象(Array-like Object)使用数组方法
var domNodeArrays = Array.prototype.slice.call(domNodes);
// 上面代码等同于
var arr = [].slice.call(arguments);
ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
5、调用父构造函数实现继承
function SuperType(){
this.color=["red", "green", "blue"];
}
function SubType(){
// 核心代码,继承自SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]
var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]
在子构造函数中,通过调用父构造函数的call
方法来实现继承,于是SubType
的每个实例都会将SuperType
中的属性复制一份。
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
更多继承方案查看我之前的文章。JavaScript常用八种继承方案
实现call 和 apply
只要实现下面3步就可以模拟实现了。
- 1、将函数设置为对象的属性:
foo.fn = bar
- 2、执行函数:
foo.fn()
- 3、删除函数:
delete foo.fn
细节 - 1、this 参数可以传 null 或者 undefined,此时 this 指向 window
- 2、this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
- 3、函数是可以有返回值的
call的模拟实现
ES3:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
// var fn = Symbol(); // added
// context[fn] = this; // changed
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
// delete context[fn] // changed
return result;
}
ES6:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
let args = [...arguments].slice(1);
let result = context.fn(...args);
delete context.fn
return result;
}
apply的模拟实现
ES3:
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
var result;
// 判断是否存在第二个参数
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')');
}
delete context.fn
return result;
}
ES6:
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn
return result;
}
模拟实现改进
当然是有问题的,其实这里假设 context
对象本身没有 fn
属性,这样肯定不行,我们必须保证 fn
属性的唯一性。
ES3下模拟实现
解决方法也很简单,首先判断 context
中是否存在属性fn
,如果存在那就随机生成一个属性fnxx
,然后循环查询 context对象中是否存在属性fnxx
。如果不存在则返回最终值。
一种循环方案实现代码如下:
function fnFactory(context) {
var unique_fn = "fn";
while (context.hasOwnProperty(unique_fn)) {
unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
}
return unique_fn;
}
一种递归方案实现代码如下:
function fnFactory(context) {
var unique_fn = "fn" + Math.random();
if(context.hasOwnProperty(unique_fn)) {
// return arguments.callee(context); ES5 开始禁止使用
return fnFactory(context); // 必须 return
} else {
return unique_fn;
}
}
ES6下模拟实现
ES6有一个新的基本类型Symbol
,表示独一无二的值,用法如下。
const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');
console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false
不能使用new
命令,因为这是基本类型的值,不然会报错。
new Symbol();
// TypeError: Symbol is not a constructor
模拟实现完整代码如下:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
var fn = Symbol(); // added
context[fn] = this; // changed
let args = [...arguments].slice(1);
let result = context[fn](...args); // changed
delete context[fn]; // changed
return result;
}
// 测试用例在下面
bind
bind 有如下特性:
- 1、可以指定this
- 2、返回一个函数
- 3、可以传入参数
- 4、柯里化
使用场景
1、业务场景
经常有如下的业务场景
var nickname = "Kitty";
function Person(name){
this.nickname = name;
this.distractedGreeting = function() {
setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}, 500);
}
}
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty
这里输出的nickname
是全局的,并不是我们创建 person
时传入的参数,因为 setTimeout
在全局环境中执行(不理解的查看【进阶3-1期】),所以this
指向的是window
。
这边把 setTimeout
换成异步回调也是一样的,比如接口请求回调。
解决方案有下面两种。
解决方案1:缓存 this值
var self = this; // added
setTimeout(function(){
console.log("Hello, my name is " + self.nickname); // changed
}, 500);
解决方案2:使用 bind
setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}.bind(this), 500);
2、判断数据类型
【进阶3-3期】介绍了 call 的使用场景,这里重新回顾下。
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
// 直接使用 toString()
[1, 2, 3].toString(); // "1,2,3"
"123".toString(); // "123"
123.toString(); // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"
可以通过toString()
来获取每个对象的类型,但是不同对象的toString()
有不同的实现,所以通过 Object.prototype.toString()
来检测,需要以call() / apply()
的形式来调用,传递要检查的对象作为第一个参数。
另一个验证是否是数组的方法,这个方案的优点是可以直接使用改造后的toStr
。
var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){
return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
// 使用改造后的 toStr
toStr([1, 2, 3]); // "[object Array]"
toStr("123"); // "[object String]"
toStr(123); // "[object Number]"
toStr(Object(123)); // "[object Number]"
上面方法首先使用 Function.prototype.call
函数指定一个 this
值,然后 .bind 返回一个新的函数,始终将 Object.prototype.toString
设置为传入参数。其实等价于 Object.prototype.toString.call()
。
3、柯里化(curry)
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
可以一次性地调用柯里化函数,也可以每次只传一个参数分多次调用。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3
这里定义了一个add
函数,它接受一个参数并返回一个新的函数。调用 add
之后,返回的函数就通过闭包的方式记住了add
的第一个参数。所以说 bind
本身也是闭包的一种使用场景。
模拟实现
首先我们来实现以下四点特性:
- 1、可以指定this
- 2、返回一个函数
- 3、可以传入参数
- 4、柯里化
bind 初版
// 第二版
Function.prototype.bind2 = function (context) {
var self = this;
// 实现第3点,因为第1个参数是指定的this,所以只截取第1个之后的参数
// arr.slice(begin); 即 [begin, end]
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 实现第4点,这时的arguments是指bind返回的函数传入的参数
// 即 return function 的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply( context, args.concat(bindArgs) );
}
}
bind
有以下一个特性:
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
举例说明:
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20
obj.habit;
// shopping
obj.friend;
// kevin
bind 最终版
// 最终版
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {}; // added
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); // updated
}
fNOP.prototype = this.prototype; // added
fBound.prototype = new fNOP(); // added
return fBound;
}
new 模拟实现
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。(下面这段跟下面特征是一样意思)
- 创建(或者说构造)一个新对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
new
创建的实例有以下 2 个特性
- 1、访问到构造函数里的属性 (apply改变this指向)
- 2、访问到原型里的属性 (指向原型)
构造函数返回值有如下三种情况:
- 1、返回一个对象 (只能访问到该对象)
- 2、没有 return,即返回 undefined (访问原对象)
- 3、返回undefined 以外的基本类型 (访问原对象)
情况1
function Car(color, name) {
this.color = color;
return {
name: name
}
}
var car = new Car("black", "BMW");
car.color;
// undefined
car.name;
// "BMW"
最终实现
function create() {
// 1.创建一个空的对象
// var obj = new Object(),
// 2.获得构造函数,arguments中去除第一个参数
Con = [].shift.call(arguments);
// 3.链接到原型,obj 可以访问到构造函数原型中的属性
// obj.__proto__ = Con.prototype;
var obj = Object.create(Con.prototype); //取代第1、3步
// 4.绑定 this 实现继承,obj 可以访问到构造函数中的属性
var ret = Con.apply(obj, arguments);
// 5.优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
解析:
1.对于⑷构造函数Con.apply(obj,arguments)
,相当于把obj
指向构造函数的this
,就可以使用构造函数的属性了
2.对于⑸,这里分三种情况
构造函数返回值有如下三种情况:
5. 返回一个对象 (会返回new 构造函数 的 return {}的对象)
6. 没有 return
,即返回 undefined
7. 返回undefined
以外的基本类型
原型/原型链
Object.create() 理解
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。
可以用babel看下class
继承的原理,有create的用法.
function Parent() {
this.age = 50
}
Parent.prototype.name = 'xm'
var p = new Parent()
var child = Object.create(p)
console.log(p) // Parent {age: 50}
console.log(child) // Parent {}
console.log(child.age) // 50
console.log(child.name) // xm
var child2 = Object.create(Parent.prototype)
console.log(child2) // Parent {}
console.log(child2.age) // undefined
console.log(child2.name) // xm
注意 第一个多了一层__proto__
(因为是那是new
出来的对象, 这个对象也有自己的原型), 这个使用当前对象给了 将要创建对象 的 __proto__
(即将要创建对象 的 __proto__
指向传入的参数对象)
如果传入参数为null
会怎样?
var child2 = Object.create(null)
child2.name = 'xm'
console.log(child2)
console.log(child2 instanceof Object) // false
console.log(child2.hasOwnProperty('name')) // Uncaught TypeError: child2.hasOwnProperty is not a function
console.log(Object.hasOwnProperty.call(child2,'name')) // true
因为与Object
的原型链断开的, 因此不能使用Object
的方法, 需要使用call/apply
指定this
Polyfill
if (typeof Object.create !== "function") {
Object.create = function (proto, propertiesObject) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object: ' + proto);
} else if (proto === null) {
throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
}
if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");
// ---------------------
function F() {}
F.prototype = proto;
return new F();
};
}
原型 prototype
JavaScript
是一种基于原型的语言 (prototype-based language),这个和 Java
等基于类的语言不一样。
每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype
属性上,而非对象实例本身。(作用: 共享数据,节省内存空间)
function foo() {
this.name = 'wo'
}
foo.prototype.hello = function() {
console.log('hello')
}
var fo = new foo()
console.log(fo)
console.log(fo.__proto__)
console.log(fo.__proto__ === foo.prototype) // true
console.log(fo.__proto__.constructor === foo.prototype.constructor) // true
function Parent() {}
var p = new Parent();
p.__proto__ === Parent.prototype
// true
所以构造函数 Parent
、Parent.prototype
和 p
的关系如下图。
构造函数 Parent
有一个指向原型的指针,原型 Parent.prototype
有一个指向构造函数的指针 Parent.prototype.constructor
,如上图所示,其实就是一个循环引用。
__proto__
上图可以看到 Parent
原型( Parent.prototype
)上有__proto__
属性,这是一个访问器属性(即 getter
函数和setter
函数),通过它可以访问到对象的内部 [[Prototype]]
(一个对象或 null
)。
__proto__
发音 dunder proto,最先被 Firefox使用,后来在 ES6 被列为 >Javascript 的标准内建属性
这里用p.__proto__
获取对象的原型,__proto__
是每个实例上都有的属性,prototype
是构造函数的属性,这两个并不一样,但 p.__proto__
和 Parent.prototype
指向同一个对象。
如果要读取或修改对象的 [[Prototype]]
属性,建议使用如下方案,但是此时设置对象的[[Prototype]]
依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作
// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()
// 修改
Object.setPrototypeOf()
Reflect.setPrototypeOf()
如果要创建一个新对象,同时继承另一个对象的[[Prototype]]
,推荐使用 Object.create()
。
function Parent() {
age: 50
};
var p = new Parent();
var child = Object.create(p);
这里 child
是一个新的空对象,有一个指向对象p
的指针__proto__
。
注意:
__proto__
是每个实例上都有的属性,prototype
是构造函数的属性,在实例上并不存在,所以这两个并不一样,但p.__proto__
和Parent.prototype
指向同一个对象。
原型链
每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null
,这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true
这里 p.constructor
指向 Parent
,那是不是意味着 p
实例存在 constructor
属性呢?并不是。
我们打印下 p
值就知道了。
由图可以看到实例对象p
本身没有 constructor
属性,是通过原型链向上查找 __proto__
,最终查找到 constructor
属性,该属性指向 Parent
。
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p; // Parent {age: 50}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true
图展示了原型链的运作机制。
小结
-
Symbol
作为构造函数来说并不完整,因为不支持语法new Symbol()
,但其原型上拥有constructor
属性,即Symbol.prototype.constructor
。 -
引用类型
constructor
属性值是可以修改的,但是对于基本类型来说是只读的,当然null
和undefined
没有constructor
属性。 -
__proto__
是每个实例上都有的属性,prototype
是构造函数的属性,在实例上并不存在,所以这两个并不一样,但p.__proto__
和Parent.prototype
指向同一个对象。 -
__proto__
属性在 ES6 时被标准化,但因为性能问题并不推荐使用,推荐使用Object.getPrototypeOf()
。 -
每个对象拥有一个原型对象,通过
__proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null
,这就是原型链。
再探原型链及其继承优缺点
原型链
上篇文章中我们介绍了原型链的概念,即每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null
,这种关系被称为原型链(prototype chain)。
根据规范不建议直接使用 _proto__
,推荐使用Object.getPrototypeOf()
,不过为了行文方便逻辑清晰,下面都以 __proto__
代替。
注意上面的说法,原型上的方法和属性被 继承 到新对象中,并不是被复制到新对象,我们看下面这个例子。
function Foo(name) {
this.name = name;
}
Foo.prototype.getName = function() {
return this.name;
}
Foo.prototype.length = 3;
let foo = new Foo('muyiy'); // 相当于 foo.__proto__ = Foo.prototype
console.dir(foo);
原型上的属性和方法定义在prototype
对象上,而非对象实例本身。当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null
)。
比如调用 foo.valueOf()
会发生什么?
- 首先检查
foo
对象是否具有可用的valueOf()
方法。 - 如果没有,则检查
foo
对象的原型对象(即Foo.prototype
)是否具有可用的valueof()
方法。 - 如果没有,则检查
Foo.prototype
所指向的对象的原型对象(即Object.prototype
)是否具有可用的valueOf()
方法。这里有这个方法,于是该方法被调用。
prototype
和 __proto__
上篇文章介绍了prototype
和__proto__
的区别,其中原型对象 prototype
是构造函数的属性,__proto__
是每个实例上都有的属性,这两个并不一样,但 foo.__proto__
和 Foo.prototype
指向同一个对象。
这次我们再深入一点,原型链的构建是依赖于prototype
还是 __proto__
呢?
Foo.prototype
中的prototype
并没有构建成一条原型链,其只是指向原型链中的某一处。原型链的构建依赖于 __proto__
,如上图通过 foo.__proto__
指向 Foo.prototype
,foo.__proto__.__proto__
指向 Bichon.prototype
,如此一层一层最终链接到 null
。
可以这么理解 Foo,我是一个 constructor,我也是一个 function,我身上有着 prototype 的 reference,只要随时调用 foo = new Foo(),我就会将
foo.__proto__
指向到我的 prototype 对象。
不要使用 Bar.prototype = Foo
,因为这不会执行 Foo
的原型,而是指向函数 Foo
。 因此原型链将会回溯到Function.prototype
而不是 Foo.prototype
,因此 method
方法将不会在 Bar
的原型链上。
function Foo() {
return 'foo';
}
Foo.prototype.method = function() {
return 'method';
}
function Bar() {
return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函数
let bar = new Bar();
console.dir(bar);
bar.method(); // Uncaught TypeError: bar.method is not a function
instanceof 原理及实现
instanceof
运算符用来检测 constructor.prototype
是否存在于参数 object
的原型链上。
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
instanceof
原理就是一层一层查找 __proto__
,如果和 constructor.prototype
相等则返回 true
,如果一直没有查找成功则返回false
。
instance.[__proto__...] === instance.constructor.prototype
知道了原理后我们来实现 instanceof
,代码如下。
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
// Object.prototype.__proto__ === null
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
/*
function instance_of(L, R) {
var O = R.prototype
while (L && (L = L.__proto__)) {
if (L === O) {
return true
}
}
return false
}
*/
// 测试
function C(){}
function D(){}
var o = new C();
instance_of(o, C); // true
instance_of(o, D); // false
JS继承
结合借用构造函数传递参数和寄生模式实现继承
这是最成熟的方法,也是现在库实现的方法
// 这是create的polyfill
function _create(proto){
function F(){}
F.prototype = proto;
return new F();
}
function extend(subClass,superClass){
var prototype = _create(superClass.prototype);//1.创建对象,创建父类原型的一个副本
prototype.constructor = subClass;//2.增强对象,弥补因重写原型而失去的默认的constructor 属性
subClass.prototype = prototype;//3.指定对象,将新创建的对象赋值给子类的原型
// 或者
// subClass.prototype = _create(superClass.prototype);
// subClass.prototype.constructor = subClass
}
// 4.借用构造函数传递增强子类实例属性(支持传参和避免篡改)
// subConstructor.call(this,arg);
Object.create()实现继承
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类的方法
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - 子类(subclass)
function Rectangle() {
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
Shape.call(this); // call super constructor. ---->注意注意这里<---
}
// 子类续承父类 ---->注意注意这里<---
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
console.log('Is rect an instance of Rectangle?',
rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
原型链继承(这里主要讲它的缺陷)
原型链继承的本质是重写原型对象,代之以一个新类型的实例。如下代码,新原型Cat
不仅有 new Animal()
实例上的全部属性和方法,并且由于指向了 Animal
原型,所以还继承了Animal
原型上的属性和方法。
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
// 这里是关键,创建 Animal 的实例,并将该实例赋值给 Cat.prototype
// 相当于 Cat.prototype.__proto__ = Animal.prototype
Cat.prototype = new Animal();
var instance = new Cat();
instance.value = 'cat'; // 创建 instance 的自身属性 value
console.log(instance.run()); // cat is runing
原型链继承方案有以下缺点:
- 1、多个实例对引用类型的操作会被篡改
- 2、子类型的原型上的 constructor 属性被重写了
- 3、给子类型原型添加属性和方法必须在替换原型之后
- 4、创建子类型实例时无法向父类型的构造函数传参
问题 1
原型链继承方案中,原型实际上会变成另一个类型的实例,如下代码,Cat.prototype
变成了 Animal
的一个实例,所以 Animal
的实例属性 names
就变成了 Cat.prototype
的属性。
而原型属性上的引用类型值会被所有实例共享,所以多个实例对引用类型的操作会被篡改。如下代码,改变了 instance1.names
后影响了 instance2
。
function Animal(){
this.names = ["cat", "dog"];
}
function Cat(){}
Cat.prototype = new Animal();
var instance1 = new Cat();
instance1.names.push("tiger");
console.log(instance1.names); // ["cat", "dog", "tiger"]
var instance2 = new Cat();
console.log(instance2.names); // ["cat", "dog", "tiger"]
问题 2
子类型原型上的constructor
属性被重写了,执行 Cat.prototype = new Animal()
后原型被覆盖,Cat.prototype
上丢失了 constructor
属性, Cat.prototype
指向了 Animal.prototype
,而 Animal.prototype.constructor
指向了 Animal
,所以 Cat.prototype.constructor
指向了 Animal
。
Cat.prototype = new Animal();
Cat.prototype.constructor === Animal
// true
解决办法就是重写 Cat.prototype.constructor
属性,指向自己的构造函数Cat
。
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
// 新增,重写 Cat.prototype 的 constructor 属性,指向自己的构造函数 Cat
Cat.prototype.constructor = Cat;
问题 3
给子类型原型添加属性和方法必须在替换原型之后,原因在第二点已经解释过了,因为子类型的原型会被覆盖。
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 新增
Cat.prototype.getValue = function() {
return this.value;
}
var instance = new Cat();
instance.value = 'cat';
console.log(instance.getValue()); // cat
属性遮蔽
改造上面的代码,在 Cat.prototype
上添加 run
方法,但是 Animal.prototype
上也有一个 run
方法,不过它不会被访问到,这种情况称为属性遮蔽 (property shadowing)。
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 新增
Cat.prototype.run = function() {
return 'cat cat cat';
}
var instance = new Cat();
instance.value = 'cat';
console.log(instance.run()); // cat cat cat
那如何访问被遮蔽的属性呢?通过__proto__
调用原型链上的属性即可。
// 接上
console.log(instance.__proto__.__proto__.run()); // undefined is runing
其他继承方案
原型链继承方案有很多问题,实践中很少会单独使用,日常工作中使用 ES6 Class extends(模拟原型)继承方案即可,更多更详细的继承方案可以阅读
JavaScript 常用八种继承方案.
小结
-
每个对象拥有一个原型对象,通过
__proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null
,这种关系被称为**原型链 ** -
当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(
null
)。 -
原型链的构建依赖于
__proto__
,一层一层最终链接到null
。 -
instanceof
原理就是一层一层查找__proto__
,如果和constructor.prototype
相等则返回true
,如果一直没有查找成功则返回false
。 -
原型链继承的本质是重写原型对象,代之以一个新类型的实例。
拓展(判断数组方法优劣)
有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
参考答案:点击查看