共同点:改变 this 指向
区别:
call 和 apply 的区别:传参的方式不同
bind 与 call / apply 的区别:
需要注意的: 如果第一个参数是 null / undefined , 则在函数体内会指向 全局对象 ,在浏览器中指向 window 。
举个栗子:
var user = {
name: 'zhang',
myAge: this.age,
myFun: function() {
console.log(`My name is ${this.name}, ${this.age} years old.`);
},
myFun2(from, to) {
console.log(`My name is ${this.name}, ${this.age} years old. I'm from ${from} , I wanna go to ${to}`);
}
};
var db = {
name: 'Emma',
age: 66
}
user.myFun.call(db);
user.myFun.apply(db);
user.myFun.bind(db)();
// My name is Emma, 66 years old.
user.myFun2.call(db, 'beijing', 'qinghai');
user.myFun2.apply(db, ['beijing', 'qinghai']);
user.myFun2.bind(db, 'beijing', 'qinghai')();
// My name is Emma, 66 years old. I'm from beijing , I wanna go to qinghai
var arr1 = [1,2,3,4];
var arr2 = ['a', 'b', 'c', 'd'];
var arr3 = [];
console.log(Array.prototype.push.call(arr3, ...arr1, ...arr2));
// arr3 [1, 2, 3, 4, 'a', 'b', 'c', 'd']
console.log(Array.prototype.push.apply(arr1, arr2));
// arr1 [1, 2, 3, 4, 'a', 'b', 'c', 'd']
console.log(Array.prototype.push.bind(arr3, ...arr1, ...arr2)());
// arr3 [1, 2, 3, 4, 'a', 'b', 'c', 'd']
console.log(Math.max.call(Math, ...randomArr)); // 45
console.log(Math.min.apply(Math, randomArr)); //1
function getDataType(data) {
return Object.prototype.toString.call(data);
// 返回值:
// "[object Object]"
// "[object Array]"
// "[object Function]"
// "[object String]"
// "[object Number]"
// "[object Null]"
// "[object Undefined]"
}
ps: 常见的伪数组有:arguments 对象 、 调用 document.getElementsByTagName / document.childNodes 等获取的节点数组等。
Array.prototype.slice.call(伪数组对象);
eg.
let arr = document.getElementsByTagName("*"); // Array.isArray(arr) --> false
let arr2 = Array.prototype.slice.call(arr); // Array.isArray(arr2) --> true
再举个例子:定义一个 log 方法,可以代理 console.log 方法
function log() {
//方式一
console.log(...arguments);
//方式二
console.log.apply(console, arguments);
// 方式三:要求在每个log前都加上(app)标识
let args = Array.prototype.slice.call(arguments);
args.unshift('(app)');
console.log.apply(null, args);
}
eg1.
let myWrite = document.write;
myWrite('write sth.'); // Uncaught TypeError: Illegal invocation(不合法的调用)
//可修改为酱紫:
myWrite.bind(document)('write sth.'); //调用myWrite的时候,将this指向为document
eg2.
var myModule = {
num: 8,
getNum() {
console.log('this.num is ' + this.num);
}
}
this.num = 9;
let getNum = myModule.getNum;
getNum(); // this.num is 9
getNum.bind(myModule)(); // this.num is 8
eg3.
//单体模式,通常会用 _this / that / self 等保存 this, 以便以后可以继续引用它
//通常的写法:
var foo = {
bar: 1,
eventBind() {
let that = this;
$('.class').off().on("click", function(ev) {
console.log(that.bar);
});
}
}
//用 bind 可以更加优雅的解決这个问题
var foo = {
bar: 1,
eventBind() {
$('.class').off().on("click", function(ev) {
console.log(this.bar);
}.bind(this));
}
}
function list() {
return Array.prototype.slice.apply(arguments);
}
let list1 = list(1,2,3,4); //[ 1, 2, 3, 4 ]
let partialList = list.bind(null, 66);
partialList(5, 6, 8); //[ 66, 5, 6, 8 ]
partialList(8, 8, 6); //[ 66, 8, 8, 6 ]
function Bloomer() {
this.petalCount = Math.floor(Math.random() * 12) + 1;
}
Bloomer.prototype.declare = function() {
console.log(`我有 ${this.petalCount} 片花瓣!`);
}
Bloomer.prototype.bloom = function() {
setTimeout(this.declare.bind(this),1000);
}
let bloomer = new Bloomer();
bloomer.bloom();
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
console.log(`x is ${this.x}, y is ${this.y}`);
}
let emptyObj = {};
let BindPoint = Point.bind(emptyObj, 0);
let bp = new BindPoint(10);
第一次实现: 简单实现:
Function.prototype.myBind = function(context) {
let self = this;
return function() {
return self.apply(context, arguments);
}
}
上面的实现方法不能实现绑定时传参的功能
第二次实现: 实现自定义的 bind 函数能够传递默认参数(即实现柯里化),返回的新函数能够接收剩余参数
Function.prototype.myBind2 = function (context) {
let self = this;
let outerArgs = Array.prototype.slice.call(arguments, 1);
return function () {
let innerArgs = Array.prototype.slice.call(arguments);
return self.apply(context, [...outerArgs, ...innerArgs]);
}
}
或者:
Function.prototype.myBind2 = function (context) {
let self = this;
let outerArgs = Array.prototype.slice.call(arguments, 1);
return function () {
let innerArgs = Array.prototype.slice.call(arguments);
let finalArgs = outerArgs.concat(innerArgs);
return self.apply(context, finalArgs);
}
}
3. 第三次实现:
上面的实现方法虽然 能够实现原生bind的基本功能,但是不能实现下面的功能:
Foo.prototype.food = 'foo-food';
var Foo = function() {
console.log(this.food);
}
var obj = {
food: 'obj-food'
};
//原生的 bind 方法的执行结果如下
var BFoo = Foo.bind(obj);
new BFoo(); // foo-food 当通过new操作符来执行 BFoo 时,this指向的是Foo
BFoo(); // obj-foo 当把BFoo当作普通函数调用时,this 指向的是给定的对象
//第二种自定义bind实现方法的执行结果如下
var BFoo2 = Foo.myBind2(obj);
new BFoo2(); // obj-food
BFoo2(); // obj-food
为了解決上面的问题,将第二种方式改写为:
Function.prototype.myBind4 = function(context) {
let outerArgs = Array.prototype.slice.call(arguments, 1);
let self = this;
if (typeof self !== 'function') {
throw new TypeError(`${self} is not a function!`);
}
var F = function() {}; // 作为中间层
let bound = function() {
let innerArgs = Array.prototype.slice.call(arguments);
let args = outerArgs.concat(innerArgs);
// Q: ctx 为什么要加个判断呢?
// A: 当 this 是 bound 的实例的时候(即 通过 new 操作符来执行bound函数的时候),
// 给定的(绑定的)对象是 bound
// (还应将 bound 的原型指向 self.prototype,
// 这样当使用 new 操作符的时候,指向的就会是实例本身,而不是bind函数绑定的对象),
// 当 this 不是 bound 的实例的时候(通过普通函数调用),ctx 应该指向的是给定的对象
let ctx = this instanceof bound? this : context;
return self.apply(ctx, args);
}
F.prototype = self.prototype;
bound.prototype = new F(); // F 函数作为中间层,防止修改 bound 原型上的属性时,将 self.prototype上的属性修改
return bound;
}
最终版(在上面的基础上再加些细节上的判断):
if (!Function.prototype.bind) {
Function.prototype.bind= function(context) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not a function!`);
}
let outerArgs = Array.prototype.slice.call(arguments, 1);
// 这里也可以简写为 :
// var outerArgs = [].slice.call(arguments, 1);
let self = this;
let F = function() {};
let Bound = function() {
let innerArgs = [].slice.call(arguments);
let args = outerArgs.concat(innerArgs);
return self.apply(this instanceof Bound ? this : context, args);
};
if (self.prototype) {
F.prototype = self.prototype;
}
Bound.prototype = new F();
return Bound;
}
}
Function.prototype.myCall = function(context) {
context = context ? context : window;
//如果 context 为 null / undefined ,则指向window;
context.fn = this;
var args = new Array(arguments);
args.splice(0, 1);
var res = context.fn(...args);
//删除该方法,不然会对传入对象造成污染(添加该方法)
delete context.fn;
//原理:如果函数是在某个上下文对象中调用的,this 绑定的是那个上下文对象
return res;
}
Function.prototype.myApply - function(context) {
context = context ? context : window;
context.fn = this;
var args = arguments[1];
context.fn(...args);
}