You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。
所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本。
寄生式组合继承写法上和组合继承基本类似,区别是如下这里:
- Dog.prototype = new Animal()- Dog.prototype.constructor = Dog+ function F() {}+ F.prototype = Animal.prototype+ let f = new F()+ f.constructor = Dog+ Dog.prototype = f
Array.prototype.forEach2=function(callback,thisArg){if(this==null){thrownewTypeError('this is null or not defined')}if(typeofcallback!=="function"){thrownewTypeError(callback+' is not a function')}constO=Object(this)// this 就是当前的数组constlen=O.length>>>0// 后面有解释letk=0while(k<len){if(kinO){callback.call(thisArg,O[k],k,O);}k++;}}
O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型。感兴趣可以阅读 something >>> 0是什么意思?。
map
基于 forEach 的实现能够很容易写出 map 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.map2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);+ res[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
+ return res
}
filter
同样,基于 forEach 的实现能够很容易写出 filter 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.filter2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);+ if (callback.call(thisArg, O[k], k, O)) {+ res.push(O[k]) + }
}
k++;
}
+ return res
}
some
同样,基于 forEach 的实现能够很容易写出 some 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.some2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);+ if (callback.call(thisArg, O[k], k, O)) {+ return true+ }
}
k++;
}
+ return false
}
reduce
Array.prototype.reduce2=function(callback,initialValue){if(this==null){thrownewTypeError('this is null or not defined')}if(typeofcallback!=="function"){thrownewTypeError(callback+' is not a function')}constO=Object(this)constlen=O.length>>>0letk=0,accif(arguments.length>1){acc=initialValue}else{// 没传入初始值的时候,取数组中第一个非 empty 的值为初始值while(k<len&&!(kinO)){k++}if(k>len){thrownewTypeError('Reduce of empty array with no initial value');}acc=O[k++]}while(k<len){if(kinO){acc=callback(acc,O[k],k,O)}k++}returnacc}
bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
实现要点:
bind() 除了 this 外,还可传入多个参数;
bing 创建的新函数可能传入多个参数;
新函数可能被当做构造函数调用;
函数可能有返回值;
Function.prototype.bind2=function(context){varself=this;varargs=Array.prototype.slice.call(arguments,1);varfNOP=function(){};varfBound=function(){varbindArgs=Array.prototype.slice.call(arguments);returnself.apply(thisinstanceoffNOP ? this : context,args.concat(bindArgs));}fNOP.prototype=this.prototype;fBound.prototype=newfNOP();returnfBound;}
Object.create2=function(proto,propertyObject=undefined){if(typeofproto!=='object'&&typeofproto!=='function'){thrownewTypeError('Object prototype may only be an Object or null.')if(propertyObject==null){newTypeError('Cannot convert undefined or null to object')}functionF(){}F.prototype=protoconstobj=newF()if(propertyObject!=undefined){Object.defineProperties(obj,propertyObject)}if(proto===null){// 创建一个没有原型对象的对象,Object.create(null)obj.__proto__=null}returnobj}
实现 Object.assign
Object.assign2=function(target, ...source){if(target==null){thrownewTypeError('Cannot convert undefined or null to object')}letret=Object(target)source.forEach(function(obj){if(obj!=null){for(letkeyinobj){if(obj.hasOwnProperty(key)){ret[key]=obj[key]}}}})returnret}
Promise.any=function(promiseArr){letindex=0returnnewPromise((resolve,reject)=>{if(promiseArr.length===0)returnpromiseArr.forEach((p,i)=>{Promise.resolve(p).then(val=>{resolve(val)},err=>{index++if(index===promiseArr.length){reject(newAggregateError('All promises were rejected'))}})})})}
为什么要写这类文章
作为一个程序员,代码能力毋庸置疑是非常非常重要的,就像现在为什么大厂面试基本都问什么 API 怎么实现可见其重要性。我想说的是居然手写这么重要,那我们就必须掌握它,所以文章标题用了死磕,一点也不过分,也希望不被认为是标题党。
作为一个普通前端,我是真的写不出 Promise A+ 规范,但是没关系,我们可以站在巨人的肩膀上,要相信我们现在要走的路,前人都走过,所以可以找找现在社区已经存在的那些优秀的文章,比如工业聚大佬写的 100 行代码实现 Promises/A+ 规范,找到这些文章后不是收藏夹吃灰,得找个时间踏踏实实的学,一行一行的磨,直到搞懂为止。我现在就是这么干的。
能收获什么
这篇文章总体上分为 2 类手写题,前半部分可以归纳为是常见需求,后半部分则是对现有技术的实现;
阅读的时候需要做什么
阅读的时候,你需要把每行代码都看懂,知道它在干什么,为什么要这么写,能写得更好嘛?比如在写图片懒加载的时候,一般我们都是根据当前元素的位置和视口进行判断是否要加载这张图片,普通程序员写到这就差不多完成了。而大佬程序员则是会多考虑一些细节的东西,比如性能如何更优?代码如何更精简?比如 yeyan1996 写的图片懒加载就多考虑了 2 点:比如图片全部加载完成的时候得把事件监听给移除;比如加载完一张图片的时候,得把当前 img 从 imgList 里移除,起到优化内存的作用。
除了读通代码之外,还可以打开 Chrome 的 Script snippet 去写测试用例跑跑代码,做到更好的理解以及使用。
在看了几篇以及写了很多测试用例的前提下,尝试自己手写实现,看看自己到底掌握了多少。条条大路通罗马,你还能有别的方式实现嘛?或者你能写得比别人更好嘛?
好了,还楞着干啥,开始干活。
数据类型判断
typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。
继承
原型链继承
原型链继承存在的问题:
借用构造函数实现继承
借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。
组合继承
组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
寄生式组合继承
组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。
所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本。
寄生式组合继承写法上和组合继承基本类似,区别是如下这里:
稍微封装下上面添加的代码后:
如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
class 实现继承
数组去重
ES5 实现:
ES6 实现:
数组扁平化
数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:
现在就是要实现 flat 这种效果。
ES5 实现:递归。
ES6 实现:
深浅拷贝
浅拷贝:只考虑对象类型。
简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。
复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。
事件总线(发布订阅模式)
解析 URL 参数为对象
字符串模板
测试:
图片懒加载
与普通的图片懒加载不同,如下这个多做了 2 个精心处理:
参考:图片懒加载
函数防抖
触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。
简单版:函数内部支持使用 this 和 event 对象;
使用:
最终版:除了支持 this 和 event 外,还支持以下功能:
使用:
参考:JavaScript专题之跟着underscore学防抖
函数节流
触发高频事件,且 N 秒内只执行一次。
简单版:使用时间戳来实现,立即执行一次,然后每 N 秒执行一次。
最终版:支持取消节流;另外通过传入第三个参数,options.leading 来表示是否可以立即执行一次,opitons.trailing 表示结束调用的时候是否还要执行一次,默认都是 true。
注意设置的时候不能同时将 leading 或 trailing 设置为 false。
节流的使用就不拿代码举例了,参考防抖的写就行。
参考:JavaScript专题之跟着 underscore 学节流
函数柯里化
什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。还不懂?来举个例子。
现在就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成多次调用每次传一个参数。
偏函数
什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子:
发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现:
如上这个功能比较简单,现在我们希望偏函数能和柯里化一样能实现占位功能,比如:
_
占的位其实就是 1 的位置。相当于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我们就来写实现:JSONP
JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;
AJAX
实现数组原型方法
forEach
参考:forEach#polyfill
O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型。感兴趣可以阅读 something >>> 0是什么意思?。
map
基于 forEach 的实现能够很容易写出 map 的实现:
filter
同样,基于 forEach 的实现能够很容易写出 filter 的实现:
some
同样,基于 forEach 的实现能够很容易写出 some 的实现:
reduce
实现函数原型方法
call
使用一个指定的 this 值和一个或多个参数来调用一个函数。
实现要点:
apply
apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。
实现要点:
bind
bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
实现要点:
实现 new 关键字
new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。
实现要点:
使用:
实现 instanceof 关键字
instanceof 就是判断构造函数的 prototype 属性是否出现在实例的原型链上。
上面的 left.proto 这种写法可以换成 Object.getPrototypeOf(left)。
实现 Object.create
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
实现 Object.assign
实现 JSON.stringify
JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space,如果对这两个参数的作用还不了解,建议阅读 MDN 文档。
参考:实现 JSON.stringify
实现 JSON.parse
介绍 2 种方法实现:
eval 实现
第一种方式最简单,也最直观,就是直接调用 eval,代码如下:
但是直接调用 eval 会存在安全问题,如果数据中可能不是 json 数据,而是可执行的 JavaScript 代码,那很可能会造成 XSS 攻击。因此,在调用 eval 之前,需要对数据进行校验。
参考:JSON.parse 三种实现方式
new Function 实现
Function 与 eval 有相同的字符串参数特性。
实现 Promise
实现 Promise 需要完全读懂 Promise A+ 规范,不过从总体的实现上看,有如下几个点需要考虑到:
Promise 写完之后可以通过 promises-aplus-tests 这个包对我们写的代码进行测试,看是否符合 A+ 规范。不过测试前还得加一段代码:
全局安装:
终端下执行验证命令:
上面写的代码可以顺利通过全部 872 个测试用例。
参考:
Promise.resolve
Promsie.resolve(value) 可以将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。
参考:深入理解 Promise
Promise.reject
和 Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。
Promise.all
Promise.all 的规则是这样的:
Promise.race
Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。
Promise.allSettled
Promise.allSettled 的规则是这样:
Promise.any
Promise.any 的规则是这样:
后话
能看到这里的对代码都是真爱了,毕竟代码这玩意看起来是真的很枯燥,但是如果看懂了后,就会像打游戏赢了一样开心,而且这玩意会上瘾,当你通关了越多的关卡后,你的能力就会拔高一个层次。用标题的话来说就是:搞懂后,提升真的大。加油吧💪,干饭人
噢不,代码人。
The text was updated successfully, but these errors were encountered: