第一次经历春招(暑期实习)后,也算是见识到了大厂对 JS 手写题的偏爱,也认识到了自己对 JS 的认识的浅薄,甚至连 API 工程师都不配叫。于是我选择回炉重造,跟着掘金上的手写教程 写一遍 36 道手写题。
数据类型 对于原生的 typeof
而言,它能够正确识别的有:undefined, boolean, number, string, symbol, function
。但无法识别其他的类型,包括:null, date
。
每个对象都有一个 toString()
方法,如果是默认继承自 Object
的 toString()
方法,则最后会输出 [object type]
,其中的 type
是对象的类型,也是我们可以更加准确的识别数据类型的原理。
由于对象可能重写过这个方法(和 Object
自带的 toString()
返回值不同),所以我们通过调用原版的方法 Object.prototype.toString
来获取对象的类型。
1 2 const arr = new Array ()console .log (Object .prototype .toString .call (arr))
返回的类型是一个字符串,所以通过 split(' ')
,先将字符串转成数组。['[object', 'Array]']
。
选取后面一个字符串,去除括号并转换为小写。
1 2 3 4 5 6 7 8 function typeOf (obj ) { let res = Object .prototype .toString .call (obj).split (' ' )[1 ] res = res.substring (0 , res.length - 1 ).toLowerCase () return res } console .log (typeOf ([])) console .log (typeOf ({})) console .log (typeOf (new Date ()))
继承 原型链实现继承 1 2 3 4 5 6 7 8 9 10 11 12 function Person ( ) { this .colors = ['black' , 'white' ] } Person .prototype .getColor = function ( ) { return this .colors } function Asian ( ) {}Asian .prototype = new Person ()let asian1 = new Asian ()asian1.colors .push ('yellow' ) let asian2 = new Asian ()console .log (asian2.colors )
缺陷:
属性中的引用类型被所有实例共享 子类在实例化的时候不能给父类构造函数传参 构造函数实现继承 1 2 3 4 5 6 7 8 9 10 function Person (name ) { this .name = name this .getName = function ( ) { return this .name } } function Asian (name ) { Person .call (this , name) } Asian .prototype = new Person ()
解决了原型链继承的问题,但是还有缺陷:
方法定义在构造函数中,创建子类实例时都会创建一次方法 组合继承 1 2 3 4 5 6 7 8 9 10 11 12 13 function Person (name ) { this .name = name } Person .prototype .getName = function ( ) { return this .name } function Asian (name, age ) { Person .call (this , name) this .age = age } Asian .prototype = new Person ()Asian .prototype .constructor = Person const p = new Asian ('ethan' , 21 )
缺陷:
调用了两次父类的构造函数,包括:new Asian(), Person.call(this, name)
寄生式组合继承 为了不调用两次构造函数,选择使用通过创建空函数获取父类的副本。
1 2 3 4 5 6 7 8 - Asian .prototype = new Person () - Asian .prototype .constructor = Person + function F ( ) {} + F.prototype = Person .prototype + let f = new F () + f.constructor = Asian + Asian .prototype = f
封装:
1 2 3 4 5 6 7 8 9 10 11 function object (o ) { function F ( ) {} F.prototype = o return new F () } function inheritPrototype (child, parent ) { let prototype = object (parent.prototype ) prototype.constructor = child child.prototype = prototype } inheritPrototype (Asian , Person )
简单版:
1 2 Asian .prototype = Object .create (Person .prototype )Asian .prototype .constructor = Asian
class
继承1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Person { constructor (name ) { this .name = name } getName ( ) { return this .name } } class Asian extends Person { constructor (name, age ) { super (name) this .age = age } }
数组去重 1 2 3 4 5 6 7 8 9 function unique (arr ) { var res = arr.filter (function (item, index, array ) { return array.indexOf (item) === index }) return res } var unique = arr => [...new Set (arr)]
数组扁平化 对高维的数组进行降维打击,变成一维数组。[1, [2, [3]]] => [1, 2, 3]
使用 JS 的 Array.prototype.flat([depth])
方法,可以对数组进行降维,其中参数 depth
表示最大递归深度。
使用 Infinity
,可以展开任意深度的嵌套数组;该方法还会移除数组中的空项。不支持 IE
使用 reduce
和 concat
可以代替该方法。
x.concat(y, z)
的特性是把数组 x
和对象 y,z
拼接到一起,形成一个新的数组。
✨值得注意的是,如果待拼接的元素是数组,就会把数组中的所有元素依次放到最终数组中;如果带拼接的元素不是数组,那么就直接放到最终数组中。
1 console .log ([1 ].concat ([2 ,3 ], 4 ))
展开一层数组:
1 2 3 4 const arr = [1 , 2 , [3 , 4 ]]arr.reduce ((acc, val ) => acc.concat (val), []) const flattened = arr => [].concat (...arr)
无限维的展开:
1 2 3 4 5 6 7 8 9 10 11 12 13 const nums = [1 , [2 , [3 , [4 ]]]]function flatter (arr, d = 1 ) { if (d > 0 ) { return arr.reduce ( (acc, val ) => acc.concat (Array .isArray (val) ? flatter (val, d - 1 ) : val), [] ) } else { return arr.slice () } } console .log (flatter (nums, Infinity ))
ES5 语法:
1 2 3 4 5 6 7 8 9 10 11 function flatter (arr ) { var res = [] for (var i = 0 ; i < arr.length ; i++) { if (Array .isArray (arr[i])) { res = res.concat (flatter (arr[i])) } else { res.push (arr[i]) } } return res }
ES6 非递归:
1 2 3 4 5 6 const flatter = (arr ) => { while (arr.some (item => Array .isArray (item))) { arr = [].concat (...arr) } return arr }
深浅拷贝 浅拷贝:
1 2 3 4 5 6 7 8 9 10 const deepClone = (target ) => { if (typeof target !== 'object' ) return target const cloneTarget = Array .isArray (target) ? [] : {} for (let prop in target) { if (target.hasOwnProperty (prop)) { cloneTarget[prop] = target[prop] } } return cloneTarget }
简易版深拷贝:
1 2 3 4 5 6 7 8 9 10 const deepClone = (target ) => { if (typeof target !== 'object' ) return target const cloneTarget = Array .isArray (target) ? [] : {} for (let prop in target) { if (target.hasOwnProperty (prop)) { cloneTarget[prop] = deepClone (target[prop]) } } return cloneTarget }
简易版深拷贝的缺陷:
只考虑了普通的对象属性,不考虑内置对象(Date, RegExp
)和函数 循环引用 完整版深拷贝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const isObject = (target ) =>(typeof taget === 'object' || typeof target === 'function' ) && target !== null const deepClone = (target, map = new WeakMap () ) => { if (map.get (target)) { return target } let constructor = target.constructor if (/^(RegExp|Date)$/i .test (constructor.name )) { return new constructor (target ) } if (isObject (target)) { map.set (target, true ) const cloneTarget = Array .isArray (target) ? [] : {} for (let prop in target) { if (target.hasOwnProperty (prop)) { cloneTarget[prop] = deepClone (target[prop], map) } } } else { return target } }
发布订阅模式 发布订阅模式描述的是对象间一对多的依赖关系,当一个对象的状态发生变化,所有依赖它的对象都得到状态改变的通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class EventEmitter { constructor ( ) { this .cache = {} } on (name, fn ) { if (this .cache [name]) { this .cache [name].push (fn) } else { this .cache [name] = [fn] } } off (name, fn ) { let tasks = this .cache [name] if (tasks) { const index = tasks.findIndex ((f ) => f === fn || f.callback === fn) if (index >= 0 ) { tasks.splice (index, 1 ) } } } emit (name, once = false , ...args ) { if (this .cache [name]) { let tasks = this .cache [name].slice () for (let fn of tasks) { fn (...args) } if (once) { delete this .cache [name] } } } } const eventBus = new EventEmitter ()const fn1 = (name, age ) => { console .log (`hello, ${name} , ${age} ` ) } const fn2 = (name, age ) => { console .log (`bye, ${name} , ${age} ` ) } eventBus.on ('a' , fn1) eventBus.on ('a' , fn2) eventBus.emit ('a' , false , 'Ethan' , 21 )
URL 解析 要知道中文是无法直接作为统一资源定位符的,所以需要使用 encodeURIComponent()
对其进行编码,变成英文和数字组成的字符串。
同样的,为了正确获得这个 URI 所代表的意义,使用 decodeURIComponent()
来进行解码。
1 2 3 4 console .log (encodeURIComponent ('苏州大学' )) console .log (decodeURIComponent ('%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%9A%84%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%94%9F%E6%B4%BB' ))
了解完这两个函数之后之后开始正式手写 URL 解析为对象的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const parseParam = (url ) => { const paramsStr = /.+\?(.+)$/ .exec (url)[1 ] const paramsArr = paramsStr.split ('&' ) const paramsObj = {} paramsArr.forEach ((param ) => { if (/=/ .test (param)) { let [key, val] = param.split ('=' ) val = decodeURIComponent (val) val = /^\d+$/ .test (val) ? parseFloat (val) : val if (paramsObj.hasOwnProperty (key)) { paramsObj[key] = [].concat (paramsObj[key], val) } else { paramsObj[key] = [val] } } else { paramsObj[param] = true } }) return paramsObj }
模板字符串 我们知道 ES6 中提供了一种语法可以更加优雅地填充字符串中的变量。
1 2 const personName = 'ethan' , age = 21 const str = `I am ${personName} , ${age} years old`
我们可以手写一个函数来模仿这种往字符串中填充变量的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const render = (template, data ) => { const reg = /\{\{(\w+)\}\}/ if (reg.test (template)) { const name = reg.exec (template)[1 ] template = template.replace (reg, data[name]) return render (template, data) } return template } const template = 'I am {{name}}, {{age}} years old.' const person = { name : 'ethan' , age : 21 , } console .log (render (template, person))
图片懒加载 为了加快首屏加载的速度,通常我们会对网页上的图片实行懒加载,即只加载可视区域的图片。
网上开源的懒加载库已经很多了,现在我们来手写一个。
基本原理:
在 HTML 中把图片地址属性从 src
改成 data-src
,避免了直接加载图片。 监听网页的滚动事件,每次加载视口内的图片。 使用 getBoundingClientRect()
来获取标签相对于网站的高度,通过和 视口的高度相比,确认图片是否在视口内。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 let imgList = [...document .querySelectorAll ('img' )]const length = imgList.length const lazyLoad = ( ) => { let count = 0 return (() => { const deleteList = [] imgList.forEach ((img, index ) => { const rect = img.getBoundingClientRect () if (rect.top < window .innerHeight ) { img.src = img.dataset .src deleteList.push (index) count++ if (count == length) { document .removeEventListener ('scroll' , lazyLoad) } } }) imgList = imgList.filter ((_, index ) => !deleteList.includes (index)) })() } document .addEventListener ('scroll' , lazyLoad)
防抖 上面一个懒加载其实有一个很严重的性能缺陷,我们对 document
的滚动事件进行了监听去出发懒加载的函数。事实上当我们滚动滑轮的时候,这个监听的函数会一直触发,导致函数的执行频率太高了。
为了对这个场景进行优化,实现在事件触发n秒后在执行回调,如果在这n秒内又被触发则重新计时,这种效果就是“防抖”。这里考虑使用 setTimeout()
函数和闭包来实现防抖效果。
掘金上一个博主的比喻我觉得很好,防抖就像是法师施法需要读条,如果读条中断就需要重新读条,读完条才能释放技能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const debounce = (fn, time ) => { let timeout return function ( ) { const context = this const args = arguments clearTimeout (timeout) timeout = setTimeout (() => { fn.apply (context, args) }, time) } } const ele = document .getElementById ('my-input' )ele.addEventListener ( 'keypress' , debounce (() => console .log (1 ), 1000 ) )
通过给输入框的 keypress
事件增加防抖处理,实现了在用户结束输入后 1s,才会触发回调函数。
节流 规定在一定时间内,只能触发一次函数,如果这段时间内多次尝试触发,则只有一次生效。
这种方法也可以解决之前的滚动事件触发频率过高的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const throttle = (fn, time ) => { let context, args let pre = 0 return function ( ) { const now = new Date () context = this args = arguments if (now - pre > time) { fn.apply (context, args) pre = now } } }
这里写的是初级版,高级版见 JavaScript专题之跟着 underscore 学节流
函数柯里化 我们的目标是实现一个函数,这个函数的作用如下:
1 2 3 4 5 6 function add (a, b, c ) { return a + b + c } let addCurry = curry (add)console .log (addCurry (1 )(2 )(3 ))
可以看出来,正常我们调用 add
,应该传三个参数。
但是在柯里化之后,我们可以通过多次每次传一个参数来实现分步的调用。
1 2 3 4 5 6 7 function curry (fn ) { let judge = (...args ) => { if (args.length == fn.length ) return fn (...args) return (...arg ) => judge (...args, ...arg) } return judge }
偏函数 柯里化的低阶版,效果如下:
1 2 3 4 5 6 7 function add (a, b, c ) { return a + b + c } let partialAdd = partial (add, 1 )console .log (partialAdd (2 , 3 ))
允许我们再调用函数的时候先传入一些参数,下次再传剩下的参数,实现也比柯里化简单。
有点像是工厂函数的味道,对一个现有的函数进行二次包装
1 2 3 4 5 function partial (fn, ...args ) { return (...arg ) => { return fn (...args, ...arg) } }
JSONP 在写 JSONP 之前,先对跨域操作有一个初步的了解。
出于安全性考虑,浏览器限制了 JS 的跨源 HTTP 请求。
例如我为了获取数据,尝试在我的主页https://blog.ethanloo.cn
调一个接口 https://api.ethanloo.cn/todos
,这就是一个跨域请求,因为请求的地址和当前的域名不同。
JSONP 就是为了解决跨域请求资源而产生的解决方案,本质利用了 <script>
标签不受跨域限制。
JSONP 属于古老但成熟的解决方案,只能进行 GET 请求;CORS 是另一个解决跨域问题的方案,复杂但更加强大。
JSONP 实现流程:
利用 script
标签规避跨域:<script src="url">
客户端声明一个函数:function jsonCallback() {}
在服务端根据客户端传的信息,查找数据库,返回字符串。 客户端利用 script
标签解析为可运行的 JavaScript
代码,调用 jsonCallback()
函数。 客户端代码 🍣 :
1 2 3 4 5 6 7 <script > function jsonCallback (data ) { console .log (data); } </script > <script src ="http://localhost:3000/todos" > </script >
服务端代码 🍣 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const http = require ('http' )let data = { id : 1 , date : '2021-04-17' , desc : 'learn jsonp' , } const server = http.createServer ((request, response ) => { if (request.url == '/todos' ) { response.writeHead (200 , { 'Content-Type' : 'application/json;charset=utf-8' , }) response.end (`jsonCallback(${JSON .stringify(data)} )` ) } }) server.listen (3000 , () => { console .log ('server is running on http://localhost:3000' ) })
上面这个方法把回调函数和请求都在客户端里写死了,接下来手写一个更灵活通用的 JSONP 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const jsonp = ({ url, params, callbackName } ) => { const generateUrl = ( ) => { let src = '' for (let key in params) { if (params.hasOwnProperty (key)) { src += `${key} =${params[key]} &` } } src += `callback=${callbackName} ` return `${url} ?${src} ` } return new Promise ((resolve, reject ) => { const scriptEle = document .createElement ('script' ) scriptEle.src = generateUrl () document .body .appendChild (scriptEle) window [callbackName] = (data ) => { resolve (data) document .removeChild (scriptEle) } }) }
AJAX 异步的数据请求,实现无刷新加载数据。JS 基础面试题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const getJSON = (url ) => { return new Pormise ((resolve, reject ) => { const xhr = XMLHttpRequest ? new XMLHttpRequest () : new ActiveXObject ('Microsoft.XMLHttp' ) xhr.open ('GET' , url, false ) xhr.setRequestHeader ('Accept' , 'application/json' ) xhr.onreadystatechange = function ( ) { if (xhr.readState !== 4 ) return if (xhr.status === 200 || xhr.status === 304 ) { resolve (xhr.responseText ) } else { reject (new Error (xhr.responseText )) } } }) }
forEach ES5 中实现的遍历数组的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Array .prototype .forEach = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null' ) } if (typeof callback !== 'function' ) { throw new TypeError (callback + ' is not a fucntion' ) } 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) } k++ } }
map 和上面 forEach
原理类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Array .prototype .map = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or undefined!' ) } 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 res = [] while (k < len) { if (k in O) { res[k] = callback.call (thisArg, O[k], k, O) } k++ } return res }
filter 本质也类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Array .prototype .filter = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or undefined!' ) } 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 res = [] while (k < len) { if (callback.call (thisArg, O[k], k, O)) { res.push (O[k]) } k++ } return res }
some 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Array .prototype .some = function (callback, thisArg ) { if (this == null ) { throw new TypeError ('this is null or undefined!' ) } 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 (callback.call (thisArg, O[k], k, O)) { return true } k++ } return false }
reduce 一个很酷的累加器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Array .prototype .reduce = function (callback, initialValue ) { if (this == null ) { throw new TypeError ('this is null or undefined!' ) } if (typeof callback !== 'function' ) { throw new TypeError (callback + ' is not a function.' ) } const O = Object (this ) const len = O.length >>> 0 let acc let k = 0 if (arguments .length > 1 ) { acc = initialValue } else { while (k < len && !(k in O)) { k++ } if (k > len) { throw new TypeError ('Reduce of empty array with no initial value' ) } acc = O[k++] } while (k < len) { if (k in O) { acc = callback (acc, O[k], k, 0 ) } k++ } return acc }
call 一个用于改变函数中 this
指向的方法。
调用形式如:
1 2 3 4 5 6 7 8 9 10 11 12 13 function eat (k ) { this .weight += k } person = { name : 'ethan' , weight : 100 , } eat.call (person, 10 ) console .log (person.weight )
eat
方法本身是不知道 this
是谁的,通过 call
,我们就能让这个方法中的 this
指到我们想要的对象上去。
同时由于 eat
方法本身还需要一个参数 k
,于是在调用 call
的时候就顺便也传给他。
代码 🍣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Function .prototype .call = function (context, ...args ) { context = context || window const fn = Symbol ('fn' ) context[fn] = this const res = context[fn](...args) delete context.fn return res }
这个手写是 ES6 语法,除开用了 const
和 Symbol
以外,展开运算符 ...
也是 ES6 语法。
apply 功能和 call
类似,就是传参的方式从展开的一个个参数变成了传入一个参数的数组。
1 2 3 4 5 6 7 8 9 Function .prototype .apply = function (context, args ) { context = context || window const fn = Symbol ('fn' ) context[fn] = this const res = context[fn](...args) delete context[fn] return res }
bind 也是用来改变函数中的 this
指向,和上面的 call
和 apply
相比区别在于不会直接调用函数,返回值是一个函数。
表面上看,包一层 apply 即可。
1 2 3 4 5 Function .prototype .bind = function (context, ...args ) { return () => { this .apply (context, args) } }
这么写的问题在于:
除了在调用 bind()
的时候可以传参之外,bind()
返回的函数应该也可以接收参数 如果 bind
绑定过的函数被 new
了,那么 this
的指向又会改变 没有保留原函数在原型链上的属性和方法 升级版:
1 2 3 4 5 6 7 8 9 10 11 12 Function .prototype .bind = function (context, ...args ) { var self = this var fn = function ( ) { self.apply ( this instanceof self ? this : context, args.concat (Array .prototype .slice .call (arguments )) ) } fn.prototype = Object .create (self.prototype ) return fn }
new new
是一个用来实例化对象的关键字,比较简单,就不举例子说明了。
实现这个关键字的几个要点:
实例可以访问私有属性 实例可以访问构造函数原型所在的原型链 需要判断构造函数执行完之后的返回值是否是对象 之所以需要判断返回值类型的原因是因为 new
本身的性质。
引自 MDN 文档 :
new
关键字会进行如下的操作:
创建一个空的简单JavaScript对象(即{}
); 链接该对象(设置该对象的constructor )到另一个对象 ; 将步骤1新创建的对象作为this
的上下文 ; 如果该函数没有返回对象,则返回this
。 🍣 代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 function newOperator (cst, ...args ) { if (typeof cst !== 'function' ) { throw 'newOperator function the first param must be a function' } let obj = Object .create (cst.prototype ) let res = cst.apply (obj, args) let isObject = typeof res === 'object' && res !== null let isFunction = typeof res === 'function' return isObject || isFunction ? res : obj }
检验实例:
1 2 3 4 5 6 7 8 9 10 function Person (name ) { this .name = name } Person .prototype .sayHi = function ( ) { console .log (this .name + ': hello!' ) } const person = newOperator (Person , 'ethan' )console .log (person)person.sayHi ()
instanceof 该关键字用于检查某个对象是否是由某个构造函数实例化生成的。
我们利用原型链的知识来实现 🍣 这个关键词。
1 2 3 4 5 6 7 8 9 10 function instanceOf (obj, cst ) { let proto = obj.__proto__ while (proto) { if (proto === null ) return false if (proto === cst.prototype ) return true proto = proto.__proto__ } return false }
直接使用 __proto__
属性来获取对象原型的方法并不好,更应该提倡的是使用 Object.getPrototype()
方法来获取原型对象。
1 2 3 4 5 6 7 8 9 10 11 12 function instanceOf (obj, cst ) { let proto = Object .getPrototypeOf (obj) while (proto) { if (proto === null ) return false if (proto === cst.prototype ) return true proto = Object .getPrototypeOf (proto) } return false }
检验:
1 2 3 4 5 6 7 8 9 function Person (name ) { this .name = name } const person = new Person ('ethan' )console .log (instanceOf (person, Person )) console .log (instanceOf (person, Object )) console .log (instanceOf (person, Array ))
Object.create 完整的写法是 Object.create(proto, [propertiesObject])
,允许指定一个原型和给定一个参数对象,来创建一个新的对象。
🍣 代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Object .create = function (proto, propertiesObject = undefined ) { if (typeof proto !== 'object' && typeof proto !== 'function' ) { throw new TypeError ('Object prototype may only be an Object or null' ) } if (propertiesObject === null ) { throw new TypeError ('Cannot convert undefined or null to object' ) } function f ( ) {} f.prototype = proto const obj = new f () if (propertiesObject !== undefined ) { Object .defineProperties (obj, propertiesObject) } if (proto === null ) { Object .setPrototypeOf (obj, null ) } return obj }
检测:
1 2 3 4 5 6 7 8 9 10 11 12 const person = Object .create ( { name : 'ethan' }, { age : { value : 3 , writable : true , enumerable : true , configurable : true , }, } ) console .log (person)
Object.assign Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,它将返回目标对象。
MDN demo:
1 2 3 4 5 6 7 8 9 10 const target = { a : 1 , b : 2 };const source = { b : 4 , c : 5 };const returnedTarget = Object .assign (target, source);console .log (target);console .log (returnedTarget);
🍣 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Object .assign = function (target, ...sources ) { if (target == null ) { throw new TypeError ('Cannot conver undefined or null to object' ) } let ret = Object (target) sources.forEach ((obj ) => { if (obj != null ) { for (let key in obj) { if (obj.hasOwnProperty (key)) { ret[key] = obj[key] } } } }) return ret }
JSON.stringfy JSON.stringfy(value, [replacer], [space])
这个方法可以将一个 JavaScript 对象或值转换为 JSON 字符串。可以使用 replacer
参数选择对序列化对象进行处理,也可以使用 space
参数美化输出的字符串中的空格数。
这里不考虑实现后面两个参数
由于 value
的变化性非常强,所以我们对其进行分类讨论。
基本数据类型
undefined/symbol => undefined
boolean => 'true'/'false'
NaN/Infinity/null => 'null'
number => '数字'
string => 字符串
对象类型
function => undefined
array
,如果出现了 undefined, function, symbol
,则=> 'null'
RegExp => '{}'
Date => toJSON()
Object => toJSON()
,忽略为 undefined, function, symbol
的属性,忽略键为 symbol
的属性。
对包含循环引用的对象报错
🍣 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 JSON .stringify = function (value ) { let type = typeof value if (type !== 'object' ) { let res = value if (Number .isNaN (value) || value === Infinity ) { result = 'null' } else if ( type === 'function' || type === 'undefined' || type === 'symbol' ) { return undefined } else if (type === 'string' ) { res = '"' + res + '"' } return String (res) } else if (type === 'object' ) { if (value === null ) { return 'null' } else if (value.toJSON && typeof value.toJSON === 'function' ) { return JSON .stringify (value.toJSON ()) } else if (value instanceof Array ) { let res = [] value.forEach ((item, index ) => { if ( typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol' ) { res[index] = 'null' } else { res[index] = JSON .stringify (item) } }) res = '[' + res + ']' return res.replace (/'/g , '"' ) } else { let res = [] Object .keys (value).forEach ((item, _ ) => { if (typeof item !== 'symbol' ) { if ( value[item] !== undefined && typeof value[item] !== 'function' && typeof value[item] !== 'symbol' ) { res.push ('"' + item + '":' + JSON .stringify (value[item])) } } }) res = '{' + res + '}' return res.replace (/'/g , '"' ) } } }
JSON.parse 该函数正好和 JSON.stringfy()
倒过来,用于将 JSON 格式的字符串解析成一个对象。
第一种方式,eval
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var rx_one = /^[\],:{}\s]*$/ ;var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g ;var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g ;var rx_four = /(?:^|:|,)(?:\s*\[)+/g ;if ( rx_one.test ( json.replace (rx_two, "@" ) .replace (rx_three, "]" ) .replace (rx_four, "" ) ) ) { var obj = eval ("(" +json + ")" ); }
第二种方式,new Function
实现:
1 2 var json = '{"name":"ethan", "age":20}' ;var obj = (new Function ('return ' + json))();
Promise 来了,来了,Promise 它来了!
因为篇幅比较长,就新开了一篇文章 。
这边手写另一篇中没写到的几个 Promise 的类方法。
Promise.resolve 该方法允许将传入的一个 value
转换为一个 fulfilled
状态的 Promise 对象。
如果传入的 value
本身已经是一个 Promise 对象,就直接返回。
1 2 3 4 5 6 Promise .resolve = function (value ) { if (value instanceof Promise ) { return value } return new Promise ((resolve ) => resolve (value)) }
Promise.reject 该方法会将传入的 reason
实例化为一个 rejected
状态的 Promise 对象,无论这个 reason
是否本身就是一个 Promise。
1 2 3 Promise .reject = function (reason ) { return new Promise ((resolve, reason ) => reject (reason)) }
Promise.all 传入一个由 Promise 对象构成的数组(严格意义上只要是可迭代对象):
如果都是 fulfilled
,就返回一个状态为 fulfilled
的新 Promise 对象,它的值由原数组内所有的值组成。 只要有一个是 rejected
,就返回一个状态为 rejected
的新 Promise 对象,它的值是原数组内第一个 rejected
的 Promise 对象的值。 只要有一个是 pending
,就返回一个状态为 pending
的新 Promise 对象。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Promise .all = function (promiseArr ) { let cnt = 0 let res = [] return new Promise ((resolve, reject ) => { promiseArr.forEach ((promise, i ) => { Promise .resolve (promise).then ( (val ) => { cnt++ res[i] = val if (cnt === promiseArr.length ) { resolve (res) } }, (error ) => { reject (error) } ) }) }) }
Promise.race 返回一个数组中第一个 fulfilled
或 rejected
的 Promise 实例,并重新包装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Promise .race = function (promiseArr ) { return new Promise ((resolve, reject ) => { promiseArr.forEach ((promise ) => { Promise .resolve (promise).then ( (value ) => { resolve (value) }, (error ) => { reject (error) } ) }) }) }
Promise.allSettled 和 all
方法类似,两者区别在于,allSettled
认为无论是 fulfilled
还是 rejected
都是 settled
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Promise .allSettled = function (promiseArr ) { let res = [] return new Promise ((resolve, reject ) => { promiseArr.forEach ((promise ) => { Promise .resolve (promise).then ( (value ) => { res.push ({ status : 'fulfilled' , value, }) if (res.length === promiseArr.length ) { resolve (res) } }, (error ) => { res.push ({ status : 'rejected' , reason : error, }) if (res.length === promiseArr.length ) { resolve (res) } } ) }) }) }
Promise.any 和 all
也很像,区别在于该方法只要有一个 fulfilled
就会返回 fulfilled
的新 Promise 对象,只有当全部 rejected
的时候,才会返回一个 rejected
的新 Promise 对象,且值为 AggregateError
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Promise .any = function (promiseArr ) { let cnt = 0 return new Promise ((resolve, reject ) => { if (promiseArr.length === 0 ) { reject (new AggregateError ()) } promiseArr.forEach ((promise ) => { Promise .resolve (promise).then ( (value ) => { resolve (value) }, (error ) => { cnt++ if (cnt === promiseArr.length ) { reject (new AggregateError ('all promises were rejected' )) } } ) }) }) }
小结 持续了半个月,陆陆续续终于是吃完了这 36 个 🍣 ,整个手撕过程基本就是跟着掘金的文章 走的,也有一部分参考的 MDN 文档。在一天吃几个 🍣 的同时,我也重新学习 JavaScript 的语法,看的是一本在线的教程 ,目前 JS 语言相关的看得差不多了。
在这里强推这个在线教程,原型链的部分写得真的超级棒 👍!每一章还有很多配套的练习题(刷题怪的天堂)。
说是查漏补缺可能不太准确,严格意义上就跟我开篇说的一样,这次属于回炉重造 。收获还是很多的,不仅熟悉了各种 API,也把我之前不太理解的原型链和 event loop 的知识弄清楚了,还有困扰了我很久的 Promise 💫!
不过我就不写自己对这些知识点的理解啦,因为作者已经写的很棒了(译者也很负责!),我的拙见写出来就成了班门弄斧了。
未来学习 JS 的路还很长,stay hungry and eat 🍣!