JS前端进阶/面试

20 篇文章 0 订阅
11 篇文章 0 订阅

附加

前端 100 问:能搞懂80%的请把简历给我

Daily-Interview-Question 面试题相关推荐

vue 如何优化首页的加载速度?vue 首页白屏是什么问题引起的?如何解决呢?

this指向

我们知道this绑定规则一共有5种情况:

1、默认绑定(严格 undefined/非严格模式 this指向全局对象)
2、隐式绑定 obj.foo()
3、显式绑定 call/apply
4、new绑定
5、箭头函数绑定
其实大部分情况下可以用一句话来概括,this总是指向调用该函数的对象。

第一种情况,默认绑定

'use strict' // 严格模式this=undefined, 非严格全局对象
function foo() {
  console.log(this)
}
foo()

思考题

题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、引用类型 --> 保存在内存中,因为这种值的大小不固定,因此不能把它们保存到栈内存中,但内存地址大小的固定的,因此保存在堆内存中,在栈内存中存放的只是该对象的访问地址。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。

f sf
几个问题

问题1var a = 20;
var b = a;
b = 30;

// 这时a的值是多少? 
问题2var a = { name: '前端开发' }
var b = a;
b.name = '进阶';

// 这时a.name的值是多少
问题3var 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);

每次调用 replaceThingtheThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 eplaceThing 又调用了 theThing )。someMethod可以通过 theThing使用,someMethodunused 分享闭包作用域,尽管 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来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。(下面这段跟下面特征是一样意思)

  1. 创建(或者说构造)一个新对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么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

所以构造函数 ParentParent.prototypep 的关系如下图。

在这里插入图片描述
构造函数 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 属性值是可以修改的,但是对于基本类型来说是只读的,当然 nullundefined 没有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.prototypefoo.__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继承

JavaScript常用八种继承方案

结合借用构造函数传递参数和寄生模式实现继承

这是最成熟的方法,也是现在库实现的方法

// 这是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()

参考答案:点击查看

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值