《前端面试之道-JS篇》(下)

继承

原型和原型链

原型:每一个对象都会在其内部初始化一个属性,就是prototype(原型)属性,相似一个指针。javascript

原型链:当咱们访问一个对象的属性时,若是这个对象内部不存在这个属性,那么就会去prototype里找这个属性,如此递推下去,一直检索到 Object 内建对象。java

《js高级程序设计》:原型链就是利用原型让一个引用类型继承另外一个引用类型的属性和方法。node

原型链例子:git

function Father(){
	this.property = true;
}
Father.prototype.getFatherValue = function(){
	return this.property;
}
function Son(){
	this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father();
//Son.prototype被重写,致使Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
	return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true

//instance实例经过原型链找到了Father原型中的getFatherValue方法.
复制代码

原型链问题:es6

  • 当原型链中包含引用类型值的原型时,该引用类型值会被全部实例共享;
  • 在建立子类型(例如建立Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

继承方式推荐

借用构造函数 + 原型链 = 组合继承混合方式

在子类构造函数内部使用apply或者call来调用父类的函数便可在实现属性继承的同时,又能传递参数,又能让实例不互相影响。github

function Super(){
    this.flag = true;
}
Super.prototype.getFlag = function(){
    return this.flag;     //继承方法
}
function Sub(){
    this.subFlag = flase
    Super.call(this)    //继承属性
}
Sub.prototype = new Supe();
Sub.prototype.constructor = Sub;
var obj = new Sub();
Super.prototype.getSubFlag = function(){
    return this.flag;
}
复制代码

小问题: Sub.prototype = new Super; 会致使Sub.prototypeconstructor指向Super; 然而constructor的定义是要指向原型属性对应的构造函数的,Sub.prototypeSub构造函数的原型,因此应该添加一句纠正:Sub.prototype.constructor = Sub;面试

组合继承是 JavaScript 最经常使用的继承模式,不过它也有不足的地方: 就是不管什么状况下,都会调用两次父类构造函数: 一次是在建立子类型原型的时候, 另外一次是在子类型构造函数内部。编程

寄生组合式继承

为了下降调用父类构造函数的开销而出现, 基本思路是没必要为了指定子类型的原型而调用超类型的构造函数。json

function extend(subClass,superClass){
    //建立对象
  var prototype = object(superClass.prototype);
  prototype.constructor = subClass;//加强对象
  subClass.prototype = prototype;//指定对象
}
复制代码

extend的高效率体如今它没有调用superClass构造函数,所以避免了在subClass.prototype上面建立没必要要,多余的属性,同时原型链还能保持不变,所以还能正常使用 instanceofisPrototypeOf() 方法.segmentfault

ES6的class

其内部其实也是ES5组合继承的方式,经过call借用构造函数,在A类构造函数中调用相关属性,再用原型链的链接实现方法的继承。

class B extends A {
  constructor() {
    return A.call(this);  //继承属性
  }
}
A.prototype = new B;  //继承方法 
复制代码

ES6封装了class,extends关键字来实现继承,内部的实现原理其实依然是基于上面所讲的原型链,不过进过一层封装后,Javascript的继承得以更加简洁优雅地实现。

class ColorPoint extends Point {
//经过constructor来定义构造函数,用super调用父类的属性方法
  constructor(x, y, color) {
    super(x, y); // 等同于parent.constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 等同于parent.toString()
  }
}
复制代码

可参考:

ES5和ES6中对于继承的实现方法

JS中的原型和原型链(面试中奖率120%)

温故js系列(15)-原型&原型链&原型继承

JS原型链与继承别再被问倒了

call, apply, bind区别

callapply 都是为了解决改变 this 的指向。

call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表。当第一个参数为null、undefined的时候,默认指向window。

apply 接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。

var arr1 = [1, 2, 3, 89, 46]
var max = Math.max.call(null, arr1[0], arr1[1], arr1[2], arr1[3], arr1[4])//89

var arr2 = [1,2,3,89,46]
var max = Math.max.apply(null,arr2)//89

//至关于
obj1.fn() === obj1.fn.call/apply(obj1);
fn1() === fn1.call/apply(null)
f1(f2) === f1.call/apply(null,f2)
复制代码
//cat give dog
cat.eatFish.call(dog, '汪汪汪', 'call')

//getValue.call(a, 'yck', '24') => a.fn = getValue
let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
复制代码

call 的实现:

Function.prototype.myCall = function (context) {
  var context = context || window
  // 给 context 添加一个属性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 将 context 后面的参数取出来
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 删除 fn
  delete context.fn
  return result
}
复制代码

bind (ES5新增) 和call 很类似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。

区别:bind 方法不会当即执行,而是返回一个改变了上下文 this 后的函数。

能够经过 bind 实现 柯里化。

关于柯里化

柯里化又称部分求值(Partial Evaluation),简单来讲就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

柯里化有3个常见做用:1. 参数复用;2. 提早返回;3. 延迟计算/运行。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);  // 3
addTen(2);  // 12
复制代码

低版本中实现 bind:

if (!Function.prototype.bind) {
    Function.prototype.bind = function () {
        var self = this,                        // 保存原函数
            context = [].shift.call(arguments), // 保存须要绑定的this上下文
            args = [].slice.call(arguments);    // 剩余的参数转为数组
        return function () {                    // 返回一个新函数
            self.apply(context, [].concat.call(args, [].slice.call(arguments)));
        }
    }
}
复制代码

应用场景:

//1.将类数组转化为数组
var trueArr = Array.prototype.slice.call(arrayLike)

//2.数组追加
var arr1 = [1,2,3];
var arr2 = [4,5,6];
var total = [].push.apply(arr1, arr2);//6
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]

//3.判断变量类型
function isArray(obj){
    return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('dot') // false

//4.利用call和apply作继承
function Person(name,age){
    // 这里的this都指向实例
    this.name = name
    this.age = age
    this.sayAge = function(){
        console.log(this.age)
    }
}
function Female(){
    Person.apply(this,arguments)//将父元素全部方法在这里执行一遍就继承了
}
var dot = new Female('Dot',2)
复制代码

Promise 实现

参考: Promise 是 ES6 新增的语法,解决了回调地狱的问题。`

回调地狱:多个回调函数嵌套

Promise 不是新的语法功能,而是一种新的写法,容许将回调函数的嵌套,改为链式调用

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then
......
复制代码

Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。

优势:

  1. 有了Promise对象,就能够将异步操做以同步操做的流程表达出来,避免了层层嵌套的回调函数。
  2. Promise对象提供统一的接口,使得控制异步操做更加容易。

缺点:

  1. 没法取消Promise,一旦新建它就会当即执行,没法中途取消。
  2. 若是不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3. 当处于pending状态时,没法得知目前进展到哪个阶段(刚刚开始仍是即将完成)。

创造 Promise 实例:

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操做成功 */){
    //resolve 指 fullfilled 状态
    resolve(value);
  } else {
    reject(error);
  }
});
复制代码

Promise实例生成之后,能够用then方法分别指定resolved状态和rejected状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});
复制代码

一个简单例子:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});
复制代码

timeout方法返回一个Promise实例,表示一段时间之后才会发生的结果。过了指定的时间(ms参数)之后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

Generator 实现

形式上,Generator 函数是一个普通函数,可是有两个特征。

  1. function关键字与函数名之间有一个星号
  2. 函数体内部使用yield表达式,定义不一样的内部状态

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。

调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

// 使用 * 表示这是一个 Generator 函数
// 内部能够经过 yield 暂停代码
// 经过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
复制代码

ES6 诞生之前,异步编程的方法,大概有下面四种。

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象 Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,无论什么操做,一眼看去都是一堆then,原来的语义变得很不清楚。

所以,Generator 函数是更好的写法。Generator 函数还能够部署错误处理代码,捕获函数体外抛出的错误。

使用 Generator 函数,执行一个真实的异步任务。

var fetch = require('node-fetch');

function* gen(){
//先读取一个远程接口,而后从 JSON 格式的数据解析信息。
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

//执行
//执行 Generator 函数,获取遍历器对象
var g = gen();
//使用next方法执行异步任务的第一阶段
var result = g.next();
//因为Fetch模块返回的是一个 Promise 对象,所以要用then方法调用下一个next方法。
result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});
复制代码

async 和 await

参考:Async/Await替代Promise的6个理由

ES2017 标准引入了 async 函数,使得异步操做变得更加方便。

async 函数就是 Generator 函数的语法糖。

async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。而且返回一个 Promise

async函数对 Generator 函数的改进:

  1. 内置执行器 async函数自带执行器,不像 Generator 函数,须要调用next方法,或者用co模块,才能真正执行。
  2. 更好的语义
  3. 更好的适用性
  4. 返回值是 Promise 进一步说,async函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

async 、 await 相比直接使用 Promise :

例子:getJSON函数返回一个promise,这个promise成功resolve时会返回一个json对象。咱们只是调用这个函数,打印返回的JSON对象,而后返回”done”。

// promise
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })
makeRequest()
复制代码
//使用Async/Await
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
makeRequest()

//async函数会隐式地返回一个promise,该promise的reosolve值就是函数return的值。(示例中reosolve值就是字符串”done”)
复制代码

优点: 处理 then 的调用链可以更清晰准确的写出代码。

缺点: 滥用 await 可能会致使性能问题,由于 await 会阻塞代码,也许以后的异步代码并不依赖于前者,但仍然须要等待前者完成,致使代码失去了并发性。

相关文章
相关标签/搜索