Open yygmind opened 5 years ago
call2,apply2和bind2的实现,我觉得可以再优化一下。 就以call2为例了,这里:
Function.prototype.call2 = function (context) {
context = context || window;
context.fn = this;
// do else...
}
传入的context
如果非空时,应该也用Object()
来做一下转换,因为原生的call如果绑入的是一个基本类型如数字或者字符串,那么内部会自动转换为new Number()
或者new String()
:
function foo() {
console.log(this);
}
foo.call(100); // Number {100}
call2,apply2和bind2的实现,我觉得可以再优化一下。 就以call2为例了,这里:
Function.prototype.call2 = function (context) { context = context || window; context.fn = this; // do else... }
传入的
context
如果非空时,应该也用Object()
来做一下转换,因为原生的call如果绑入的是一个基本类型如数字或者字符串,那么内部会自动转换为new Number()
或者new String()
:function foo() { console.log(this); } foo.call(100); // Number {100}
谢谢提醒,已更新
我用原生的bind试了obj.proto.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回错误,这里被修改了// Kitty 发现也是会修改的,这个我有点蒙了哈 {friend: "Kitty", constructor: ƒ} paste.html:68 {friend: "Kitty", constructor: ƒ},打印出来是一样的东西
var Foot = bar.bind(foo, "Jack") var a = new Foot(20); a.proto.friend = "Kitty"; // 修改原型 console.log(bar.prototype.friend) console.log(bar.prototype) console.log(a.proto)
当使用 bind2 返回的函数作为构造函数时,返回的对象类型总是 fBound,而使用原生的 bind 返回的对象类型跟原来类型是一致的。
function add(num){
const sum = (arguments[1] || 0) + num;
console.log(sum);
return add.bind(this, sum)
}
// 第四版,已通过测试用例 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(
this instanceof fNOP ? this : context,
args.concat(bindArgs)
);
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
} 这里的this instanceof fNOP ? this : context, 是不是有问题?因为此时fNOP.prototype.constructor=this.prototype.constructor,而不是指向fNOP。
function Person(name){ this.nickname = name; this.distractedGreeting = function() {
setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}.bind(this), 500);
}
}
这个settimeout是再全局执行得,为什么bind中这个this不是指向window,
终于看懂bind 模拟实现了 以前看其他的文章 ,总是弄不懂 ,这个this 一会儿判断 是不是function, 一会儿又是instance of 等等 结合作者的例子总算明白了 ,给作者一个大大的赞
这里用 hasOwnProperty 来判断 unique_fn 在不在 context 里面会不会有什么问题?万一原型链上有,而执行函数的时候需要用到这个原型链上的函数,但由于 unique_fn 将其覆盖了,导致用到的不是期望的那个原型链上的方法?
call2,apply2和bind2的实现,我觉得可以再优化一下。 就以call2为例了,这里:
Function.prototype.call2 = function (context) { context = context || window; context.fn = this; // do else... }
传入的
context
如果非空时,应该也用Object()
来做一下转换,因为原生的call如果绑入的是一个基本类型如数字或者字符串,那么内部会自动转换为new Number()
或者new String()
:function foo() { console.log(this); } foo.call(100); // Number {100}
谢谢提醒,已更新
如果传个Number
0 或者 NaN
就又G了
建议这样处理
if(context === null || context === undefined) {
context = window
} else {
context = Object(context)
}
我用原生的bind试了obj.proto.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回错误,这里被修改了// Kitty 发现也是会修改的,这个我有点蒙了哈 {friend: "Kitty", constructor: ƒ} paste.html:68 {friend: "Kitty", constructor: ƒ},打印出来是一样的东西
var Foot = bar.bind(foo, "Jack") var a = new Foot(20); a.proto.friend = "Kitty"; // 修改原型 console.log(bar.prototype.friend) console.log(bar.prototype) console.log(a.proto)
我也试了一下,不懂为啥会说是返回错误,原生的bind 也修改了原型,那这里出错的原因,作者也没有解释
function Person(name){ this.nickname = name; this.distractedGreeting = function() {
setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }.bind(this), 500); }
}
这个settimeout是再全局执行得,为什么bind中这个this不是指向window,
bind 中的 this 和函数体中所有出现过的 this 都指向调用构造函数后创建的实例,这里就是用 bind 完成一个硬绑定而已
// 第四版,已通过测试用例 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply( this instanceof fNOP ? this : context, args.concat(bindArgs) ); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound;
} 这里的this instanceof fNOP ? this : context, 是不是有问题?因为此时fNOP.prototype.constructor=this.prototype.constructor,而不是指向fNOP。
这里没有问题。instanceof 中,只要右操作数的原型出现在左操作数的原型链上,就会返回 true。这里的右操作数 fNOP 的原型也就是 fNOP.prototype = new fNOP( ).__proto__ = fBound.prototype.__proto__ = new fBound( ).__proto__.proto__
,而 new fBound( ).__proto__.proto__
确实是出现在左操作数 this (也就是 new fBound( )
)的原型链上的。
bind()
语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind
方法与call / apply
最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。来个例子说明下
通过上述代码可以看出
bind
有如下特性:this
使用场景
1、业务场景
经常有如下的业务场景
这里输出的
nickname
是全局的,并不是我们创建person
时传入的参数,因为setTimeout
在全局环境中执行(不理解的查看【进阶3-1期】),所以this
指向的是window
。这边把
setTimeout
换成异步回调也是一样的,比如接口请求回调。解决方案有下面两种。
解决方案1:缓存
this
值解决方案2:使用
bind
完美!
2、验证是否是数组
【进阶3-3期】介绍了
call
的使用场景,这里重新回顾下。可以通过
toString()
来获取每个对象的类型,但是不同对象的toString()
有不同的实现,所以通过Object.prototype.toString()
来检测,需要以call() / apply()
的形式来调用,传递要检查的对象作为第一个参数。另一个验证是否是数组的方法,这个方案的优点是可以直接使用改造后的
toStr
。上面方法首先使用
Function.prototype.call
函数指定一个this
值,然后.bind
返回一个新的函数,始终将Object.prototype.toString
设置为传入参数。其实等价于Object.prototype.toString.call()
。这里有一个前提是
toString()
方法没有被覆盖3、柯里化(curry)
可以一次性地调用柯里化函数,也可以每次只传一个参数分多次调用。
这里定义了一个
add
函数,它接受一个参数并返回一个新的函数。调用add
之后,返回的函数就通过闭包的方式记住了add
的第一个参数。所以说bind
本身也是闭包的一种使用场景。模拟实现
bind()
函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8
及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现。首先我们来实现以下四点特性:
this
模拟实现第一步
对于第 1 点,使用
call / apply
指定this
。对于第 2 点,使用
return
返回一个函数。结合前面 2 点,可以写出第一版,代码如下:
测试一下
模拟实现第二步
对于第 3 点,使用
arguments
获取参数数组并作为self.apply()
的第二个参数。对于第 4 点,获取返回函数的参数,然后同第3点的参数合并成一个参数数组,并作为
self.apply()
的第二个参数。测试一下:
模拟实现第三步
到现在已经完成大部分了,但是还有一个难点,
bind
有以下一个特性来个例子说明下:
上面例子中,运行结果
this.value
输出为undefined
,这不是全局value
也不是foo
对象中的value
,这说明bind
的this
对象失效了,new
的实现中生成一个新的对象,这个时候的this
指向的是obj
。(【进阶3-1期】有介绍new的实现原理,下一期也会重点介绍)这里可以通过修改返回函数的原型来实现,代码如下:
this instanceof fBound
结果为true
,可以让实例获得来自绑定函数的值,即上例中实例会具有habit
属性。window
,此时结果为false
,将绑定函数的 this 指向context
prototype
为绑定函数的prototype
,实例就可以继承绑定函数的原型中的值,即上例中obj
可以获取到bar
原型上的friend
。注意:这边涉及到了原型、原型链和继承的知识点,可以看下我之前的文章。
JavaScript常用八种继承方案
模拟实现第四步
上面实现中
fBound.prototype = this.prototype
有一个缺点,直接修改fBound.prototype
的时候,也会直接修改this.prototype
。来个代码测试下:
解决方案是用一个空对象作为中介,把
fBound.prototype
赋值为空对象的实例(原型式继承)。这边可以直接使用ES5的
Object.create()
方法生成一个新对象不过
bind
和Object.create()
都是ES5方法,部分IE浏览器(IE < 9)并不支持,Polyfill中不能用Object.create()
实现bind
,不过原理是一样的。第四版目前OK啦,代码如下:
模拟实现第五步
到这里其实已经差不多了,但有一个问题是调用
bind
的不是函数,这时候需要抛出异常。所以完整版模拟实现代码如下:
【进阶3-2期】思考题解
【进阶3-3期】思考题解
call
的模拟实现如下,那有没有什么问题呢?当然是有问题的,其实这里假设
context
对象本身没有fn
属性,这样肯定不行,我们必须保证fn
属性的唯一性。ES3下模拟实现
解决方法也很简单,首先判断
context
中是否存在属性fn
,如果存在那就随机生成一个属性fnxx
,然后循环查询context
对象中是否存在属性fnxx
。如果不存在则返回最终值。一种循环方案实现代码如下:
一种递归方案实现代码如下:
模拟实现完整代码如下:
ES6下模拟实现
ES6有一个新的基本类型
Symbol
,表示独一无二的值,用法如下。不能使用
new
命令,因为这是基本类型的值,不然会报错。模拟实现完整代码如下:
测试用例在这里:
扩展一下
有两种方案可以判断对象中是否存在某个属性。
in
操作符in
操作符会检查属性是否存在对象及其[[Prototype]]
原型链中。Object.hasOwnProperty(...)
方法hasOwnProperty(...)
只会检查属性是否存在对象中,不会向上检查其原型链。注意以下几点:
in
操作符可以检查容器内是否有某个值,实际上检查的是某个属性名是否存在。对于数组来说,4 in [2, 4, 6]
结果返回false
,因为[2, 4, 6]
这个数组中包含的属性名是0,1,2
,没有4
。Object.prototype
的委托来访问hasOwnProperty(...)
,但是对于一些特殊对象(Object.create(null)
创建)没有连接到Object.prototype
,这种情况必须使用Object.prototype.hasOwnProperty.call(obj, "a")
,显示绑定到obj
上。又是一个call
的用法。本期思考题
用 JS 实现一个无限累加的函数
add
,示例如下:参考
进阶系列目录
交流
进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!