继承
原型和原型链
原型:每个对象都会在其内部初始化一个属性,就是prototype(原型)属性,类似一个指针。
原型链:当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么就会去prototype里找这个属性,如此递推下去,一直检索到 Object 内建对象。
《js高级程序设计》:原型链就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链例子:
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方法.
原型链问题:
- 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
- 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
继承方式推荐
借用构造函数 + 原型链 = 组合继承混合方式
在子类构造函数内部使用apply或者call来调用父类的函数即可在实现属性继承的同时,又能传递参数,又能让实例不互相影响。
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.prototype的constructor指向Super;
然而constructor的定义是要指向原型属性对应的构造函数的,Sub.prototype是Sub构造函数的原型,所以应该添加一句纠正:Sub.prototype.constructor = Sub;
组合继承是 JavaScript 最常用的继承模式,不过它也有不足的地方: 就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部。
寄生组合式继承
为了降低调用父类构造函数的开销而出现, 基本思路是不必为了指定子类型的原型而调用超类型的构造函数。
function extend(subClass,superClass){
//创建对象
var prototype = object(superClass.prototype);
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性,同时原型链还能保持不变,因此还能正常使用 instanceof 和 isPrototypeOf() 方法.
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()
}
}
可参考:
call, apply, bind区别
call 和 apply 都是为了解决改变 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(已定型)。
优点:
- 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
- Promise对象提供统一的接口,使得控制异步操作更加容易。
缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于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 函数是一个普通函数,但是有两个特征。
- function关键字与函数名之间有一个
星号; - 函数体内部使用
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
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数就是 Generator 函数的语法糖。
async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。并且返回一个
Promise。
async函数对 Generator 函数的改进:
- 内置执行器 async函数自带执行器,不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行。
- 更好的语义
- 更好的适用性
- 返回值是 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 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。