JavaScript栏目介绍一些必会操作。
JavaScript栏目介绍一些必会操作。
和其他“圈子”里的同学们不一样,前端圈子里的同学们都很热衷于“手写xxx方法”,基本上每天在掘金里都可以看到类似的文章。但是,很多文章(不代表全部,无意冒犯)大都是囫囵吞枣、依葫芦画瓢,经不起推敲和考究,很容易误导那些对JavaScript刚入门的新同学。 鉴于此,本文将基于《你不知道的JavaScript》(小黄书)里一些典型的知识点,结合一些经典的、高频的被“手写”的方法来逐一地原理和实现相结合,和同学们一起在搞懂原理的基础上再去手写代码。 一、操作符new在讲解它之前我们首先需要澄清一个非常常见的关于 JavaScript 中函数和对象的误解: 在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 something = new MyClass(..);复制代码 JavaScript 也有一个 首先我们重新定义一下 JavaScript 中的“构造函数”。在 JavaScript 中,构造函数只是一些使用 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用
因此,如果我们要想写出一个合乎理论的 /**
* @param {fn} Function(any) 构造函数
* @param {arg1, arg2, ...} 指定的参数列表
*/
function myNew (fn, ...args) {
// 创建一个新对象,并把它的原型链(__proto__)指向构造函数的原型对象
const instance = Object.create(fn.prototype)
// 把新对象作为thisArgs和参数列表一起使用call或apply调用构造函数
const result = fn.apply(instance, args)
如果构造函数的执行结果返回了对象类型的数据(排除null),则返回该对象,否则返新对象
return (result && typeof instance === 'object') ? result : instance
}
复制代码
二、操作符instanceof在相当长的一段时间里,JavaScript 只有一些近似类的语法元素,如 在不考虑 因此,我们既然搞懂了 看到这里,基本上明白了,
以下 /**
* @param {left} Object 实例对象
* @param {right} Function 构造函数
*/
function myInstanceof (left, right) {
// 保证运算符右侧是一个构造函数
if (typeof right !== 'function') {
throw new Error('运算符右侧必须是一个构造函数')
return
}
// 如果运算符左侧是一个null或者基本数据类型的值,直接返回false
if (left === null || !['function', 'object'].includes(typeof left)) {
return false
}
// 只要该构造函数的原型对象出现在实例对象的原型链上,则返回true,否则返回false
let proto = Object.getPrototypeOf(left)
while (true) {
// 遍历完了目标对象的原型链都没找到那就是没有,即到了Object.prototype
if (proto === null) return false
// 找到了
if (proto === right.prototype) return true
// 沿着原型链继续向上找
proto = Object.getPrototypeOf(proto)
}
}复制代码三、 Object.create()
在《你不知道的JavaScript》中,多次用到了 简单起见,为了和 /**
* 基础版本
* @param {Object} proto
*
*/
Object.prototype.create = function (proto) {
// 利用new操作符的特性:创建一个对象,其原型链(__proto__)指向构造函数的原型对象
function F () {}
F.prototype = proto
return new F()
}
/**
* 改良版本
* @param {Object} proto
*
*/
Object.prototype.createX = function (proto) {
const obj = {}
// 一步到位,把一个空对象的原型链(__proto__)指向指定原型对象即可
Object.setPrototypeOf(obj, proto)
return obj
}复制代码我们可以看到, 四、Function的原型方法:call、apply和bind作为最经典的手写“劳模”们, 在《你不知道的JavaScript上卷》第二部分的第1章和第2章,用了2章斤30页的篇幅中详细地介绍了 而我们要实现的 《你不知道的JavaScript》总结了四条规则来判断一个运行中函数的
更具体一点,可以描述为:
var bar = new foo()复制代码
var bar = foo.call(obj2)复制代码
var bar = obj1.foo()复制代码
var bar = foo()复制代码 就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 至此,你已经搞明白了 4.1 call和apply实现 const context = {
name: 'ZhangSan'
}
function sayName () {
console.log(this.name)
}
context.sayName = sayName
context.sayName() // ZhangSan复制代码这样,我们就完成了“隐式绑定”。落实到具体的代码实现上: /**
* @param {context} Object
* @param {arg1, arg2, ...} 指定的参数列表
*/
Function.prototype.call = function (context, ...args) {
// 指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装
if (context === null || context === undefined) {
context = window
} else if (typeof context !== 'object') {
context = new context.constructor(context)
} else {
context = context
}
const func = this
const fn = Symbol('fn')
context[fn] = func
const result = context[fn](...args)
delete context[fn]
return result
}
/**
* @param {context}
* @param {args} Array 参数数组
*/
Function.prototype.apply = function (context, args) {
// 和call一样的原理
if (context === null || context === undefined) {
context = window
} else if (typeof context !== 'object') {
context = new context.constructor(context)
} else {
context = context
}
const fn = Symbol('fn')
const func = this
context[fn] = func
const result = context[fn](...args)
delete context[fn]
return result
}复制代码细看下来,大家都那么聪明,肯定一眼就看到了它们的精髓所在: const fn = Symbol('fn')
const func = this
context[fn] = func复制代码在这里,我们使用 4.2 bind在《你不知道的JavaScript》中,手动实现了一个简单版本的 function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}复制代码硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值。 由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 function foo(something) {
console.log( this.a, something )
return this.a + something;
}
var obj = {
a:2
}
var bar = foo.bind( obj )
var b = bar( 3 ); // 2 3
console.log( b ); // 5复制代码
因此,我们可以在此基础上实现我们的 /**
* @param {context} Object 指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装
*
* @param {arg1, arg2, ...} 指定的参数列表
*
* 如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg
*/
Function.prototype.bind = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('必须使用函数调用此方法');
}
const _self = this
// fNOP存在的意义:
// 1. 判断返回的fBound是否被new调用了,如果是被new调用了,那么fNOP.prototype自然是fBound()中this的原型
// 2. 使用包装函数(_self)的原型对象覆盖自身的原型对象,然后使用new操作符构造出一个实例对象作为fBound的原型对象,从而实现继承包装函数的原型对象
const fNOP = function () {}
const fBound = function (...args2) {
// fNOP.prototype.isPrototypeOf(this) 为true说明当前结果是被使用new操作符调用的,则忽略context
return _self.apply(fNOP.prototype.isPrototypeOf(this) && context ? this : context, [...args, ...args2])
}
// 绑定原型对象
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
return fBound
}复制代码具体的实现细节都标注了对应的注释,涉及到的原理都有在上面的内容中讲到,也算是一个总结和回顾吧。 五、函数柯里化
看这个解释有一点抽象,我们就拿被做了无数次示例的 // 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3复制代码实际上就是把 函数柯里化在一定场景下,有很多好处,如:参数复用、提前确认和延迟运行等,具体内容可以拜读下这篇文章,个人觉得受益匪浅。 最简单的实现函数柯里化的方式就是使用 function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}复制代码如果用ES5代码实现的话,会比较麻烦些,但是核心思想是不变的,就是在传递的参数满足调用函数之前始终返回一个需要传参剩余参数的函数: // 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
function curry(fn, args) {
args = args || []
// 获取函数需要的参数长度
let length = fn.length
return function() {
let subArgs = args.slice(0)
// 拼接得到现有的所有参数
for (let i = 0; i < arguments.length; i++) {
subArgs.push(arguments[i])
}
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs)
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return curry.call(this, fn, subArgs)
}
};
}复制代码六、文末总结在本文中,我们熟悉了 当然,在《你不知道的JavaScript》中还有很多精妙的见解和知识内容,如"JavaScript是需要先
以上就是你所不知道的JavaScript的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |
