实现 new
先用文字描述一下 new 的实现过程
- 新定义一个 json 对象
- 对象 继承 构造函数的原型链
- 将构造函数的 this 指向这个对象
- 根据构造函数的返回值类型返回结果,
function myNew(fn) {
let obj = {
}obj = Object.create(fn.prototype) let args = Array.prototype.slice.call(arguments, 1) // 获取除去fn之外的参数let result = fn.call(obj, ...args)return typeof result === 'object' ? result : obj;}function foo() {
this.name = 'ciel'this.arg = arguments[0]}foo.prototype.callName = function() {
console.log(this.name)}// 测试let test = myNew(foo, 'hhh', '123', 'saf')test.callName()console.log(test.arg)
这里解释一下 return typeof result === ‘object’ ? result : obj; 这句代码:
在JavaScript构造函数中:如果return值类型,那么对构造函数没有影响,实例化对象返回空对象;如果return引用类型(数组,函数,对象),那么实例化对象就会返回该引用类型; 可以测试以下两个构造函数在 new 之后返回的值就可以理解这句话的意思了
function foo() {
this.name = 'ciel'return function() {
}}new foo() // fn(){}function bar() {
this.name = 'ciel'return 1}new bar() // {name: ciel}
实现 call
先看看伪代码是如何使用 myCall 的 fn.myCall(obj, args) 分析下代码应该怎么实现
myCall 应该挂在 Function.prototype 上
fn 的 this 指向 为 obj
myCall 的 args 透传给 fn
Function.prototype.myCall = function(target, ...args) {
// this 指向调用 myCall函数的对象if (typeof this !== "function") {
throw new TypeError("not a function")}target = target || windowtarget.fn = this // 隐式绑定,改变构造函数的调用者间接改变 this 指向let result = target.fn(...args)return result
};
// 测试
let obj = {
name: 123 }
function foo(...args) {
console.log(this.name, args)
}
let s = foo.myCall(obj, '111', '222')
实现 apply
回忆一下 apply 与 call 的区别: apply 参数要为数组。 其他和 call 实现一样
Function.prototype.myApply = function(target) {
if (typeof this !== "function") {
throw new TypeError("not a function");}if (!Array.isArray(arguments[1])) {
throw new Error('arg not a array')}target = target || windowtarget.fn = thislet args = arguments[1]let result = target.fn(args)return result
};const obj = {
name: 123 };
function foo(...args) {
console.log(this.name, args);
}
foo.prototype.name = 123;
const s1 = [1, 2, 3, 4, 5];
const s = foo.myApply(obj,s1);
实现 bind
与 call 与 apply 的区别: fn.bind(obj) 不会立即执行 fn 函数,而 call, apply 会立即执行
bind 返回的新函数可以普通调用也可以构造函数方式调用,当为构造函数时,this 是指向实例的
bind() 方法的参数具有一个特性,就是函数柯里化,简单来说就是保留一个参数的位置,再第二次传参的时候自动把参数存入到这个位置中
Function.prototype.mybind = function(thisArg) {
if (typeof this !== 'function') {
throw TypeError("Bind must be called on a function");}const args = Array.prototype.slice.call(arguments, 1),self = this,// 构建一个干净的函数,用于保存原函数的原型nop = function() {
},// 绑定的函数bound = function() {
// this instanceof nop, 判断是否使用 new 来调用 bound// 如果是 new 来调用的话,this的指向就是其实例,// 如果不是 new 调用的话,就改变 this 指向到指定的对象 oreturn self.apply(this instanceof nop ? this : thisArg,args.concat(Array.prototype.slice.call(arguments)) );};// 箭头函数没有 prototype,箭头函数this永远指向它所在的作用域if (this.prototype) {
nop.prototype = this.prototype;}// 修改绑定函数的原型指向bound.prototype = new nop();return bound;
}// 测试let obj = {
name: "ciel" }function test(x,y,z) {
console.log(this.name) // cielconsole.log(x+y+z) // 6}let Bound = test.mybind(obj, 1, 2)Bound(3) // 6new Bound() // bound {}
实现 reduce
arr.reduce((res,cur, index, arr) => res+cur, 0)
参数: 一个回调函数,一个初始化参数 (非必须)
回调函数参数有 4 个值(res: 代表累加值,cur: 目前值,index: 第几个,arr 调用 reduce 的数组)
整体返回 res 累加值
Array.prototype.myReduce = function(cb, initValue) {
if (!Array.isArray(this)) {
throw new TypeError("not a array")}// 数组为空,并且有初始值,报错if (this.length === 0 && arguments.length < 2) {
throw new TypeError('Reduce of empty array with no initial value')}let arr = thislet res = null// 判断有没有初始值if (arguments.length > 1) {
res = initValue} else {
res = arr.splice(0,1)[0] //没有就取第一个值}arr.forEach((item, index) => {
res = cb(res, item, index, arr) // cb 每次执行完都会返回一个新的 res值,覆盖之前的 res})return res
};// 测试结果
let arr = [1,2,3,4]
let result = arr.myReduce((res, cur) => {
return res + cur
})
console.log(result) // 10
tip: 平时在工作中 处理数据的时候经常会用到 reduce, 实现一个数据处理原本多次遍历,由 reduce 实现可能就只需要遍历一次
实现 Currying
什么是柯里化? 将复杂问题分解为多个可编程的小问题,实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数 结合一个例子,实现如下效果
sum(1,2) // 3
sum(1,2)(3) // 6
sum(4,5)(10) // 19实现代码
function sum() {
let allArgs = Array.prototype.slice.call(arguments);let add = function(){
allArgs.push(...arguments) // 每次调用 sum 函数都收集参数return add}// 重写 toString 方法,函数执行的时候会自动调用toString()方法,计算返回所有参数结果add.toString = function () {
return allArgs.reduce((a, b) => a+b)}return add
}
实现防抖
防抖:触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间(取最后一次) 思路:每次触发前都取消之前的延时调用方法
function debounce(fn, delay) {
let timer = nullreturn function() {
let self = this // 这获取 this 是因为 debounce() 返回的是内部函数,在这才能捕获到 this。let args = Array.prototype.slice.call(arguments)if (timer) clearTimeout(timer) // 取消之前的 timertimer = setTimeout(function () {
fn.call(self, ...args) // 防止 this 指向改变,确保上下文为当前的this,传递参数}, delay)}}function testFn() {
console.log('被点击了', this)}// 测试
document.addEventListener('click', debounce(testFn, 1000))
实现节流
节流:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率 思路:每次触发事件时都判断当前是否有等待执行的延时函数,需要一个标记
function throtting(fn, delay) {
let timer = nulllet isCancel = falsereturn function() {
if (isCancel) returnisCancel = trueclearTimeout(timer)let self = this;let args = Array.prototype.slice.call(arguments)if (timer) clearTimeout(timer)timer = setTimeout(function () {
fn.call(self, ...args)isCancel = false}, delay)}}function testFn() {
console.log('输入了', this)}
document.addEventListener('input', throtting(testFn, 1000))
在一定时间内只执行一次,判断当前是否有等待执行的延时函数,有就返回
实现Sleep函数
//方法一function sleep1(ms, callback) {
setTimeout(callback, ms)}//sleep 1ssleep1(1000, () => {
console.log(1000)})//方法二function sleep2(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms)})}sleep2(1000).then(() => {
console.log(2000)})//方法三function sleep3(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms)})}async function init() {
await sleep3(1000);}init().then(() => {
console.log(3000)})
实现闭包
var add = (function () {
var counter = 0;return function () {
return counter += 1;}
})();add();
add();
add();
实现继承
Javascript对象创建过程:
1.初始化对象
2.将对象的__proto__=类的prototype属性
3.用类构造对象
proto:为原型链
每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个__proto__又会有自己的__proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念。
2.将对象的__proto__ = 类的prototype属性,且prototype是一个对象。
因此为了实现继承,我们只要将对象的的原型链进行修改即可。
由于__proto__在对象创建后才可出现,其实这个属性也不能直接访问(原则上),但我们在类定义时就得确认类的继承关系。此时,我们突然发现以上加粗的公式。
类的prototype = 对象的 proto。好吧,现在只要在类的prototype上做文章即可。
可以想象,如果要加入父类的原型链,只有两种方法:
1.将子类的prototype=父类的prototype。这个非常不好,因为既然相等了,此时改变任何一个类的prototype,都会影响另一个类的prototype
2.将子类的prototype=父类的对象。这个是一个不错的想法,修改了类属性时,只是修改父类对象的一个实例,并不会修改父类的类。
JS继承的实现方式
既然要实现继承,那么首先我们得有一个父类,代码如下:// 定义一个动物类
function Animal (name) {
// 属性this.name = name || 'Animal';// 实例方法this.sleep = function(){
console.log(this.name + '正在睡觉!');}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1、原型链继承
核心: 将父类的实例作为子类的原型function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
特点:非常纯粹的继承关系,实例是子类的实例,也是父类的实例
父类新增原型方法/原型属性,子类都能访问到
简单,易于实现
缺点:要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
无法实现多继承
来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)2、构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)function Cat(name){
Animal.call(this);this.name = name || 'Tom';
}// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:解决了1中,子类实例共享父类引用属性的问题
创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)
缺点:实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3)3、实例继承
核心:为父类实例添加新特性,作为子类实例返回function Cat(name){
var instance = new Animal();instance.name = name || 'Tom';return instance;
}// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
特点:不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:实例是父类的实例,不是子类的实例
不支持多继承
推荐指数:★★4、拷贝继承
function Cat(name){
var animal = new Animal();for(var p in animal){
Cat.prototype[p] = animal[p];}this.name = name || 'Tom';
}// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:支持多继承
缺点:效率较低,内存占用高(因为要拷贝父类的属性)
无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
推荐指数:★(缺点1)5、组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用function Cat(name){
Animal.call(this);this.name = name || 'Tom';
}
Cat.prototype = new Animal();// 感谢 @学无止境c 的提醒,组合继承也是需要修复构造函数指向的。Cat.prototype.constructor = Cat;// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特点:弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用
缺点:调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)6、寄生组合继承
核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点function Cat(name){
Animal.call(this);this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类var Super = function(){
};Super.prototype = Animal.prototype;//将实例作为子类的原型Cat.prototype = new Super();
})();// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true感谢 @bluedrink 提醒,该实现没有修复constructor。Cat.prototype.constructor = Cat; // 需要修复下构造函数
特点:堪称完美
缺点:实现较为复杂