必须是
函数才能使用call、apply、bind,也就是.前边必须是函数,call、apply、bind中的this就是该函数
call、apply是把this(需要改变this指向的函数、call、apply前边的函数)绑定给context(要指向的this)的一个fn属性然后 context.fn()
bind是利用闭包把this(需要改变this指向的函数、bind前边的函数)传递到返回的函数,在执行时
- apply实现: 直接this.appl改变this的指向到context(要指向的this)
- es手动实现:也是和
call、apply一样绑定给context(要指向的this)的一个fn属性然后 context.fn()
call:fn.call(arg1, arg2, ...);
第一个参数必须是
对象数据类型fn.call :(找到call方法)当前实例(函数FN)
通过原型链的查找机制找到Function.prototype上的call方法=> function call(){[native code]}
fn.call():把通过原型链找到的call方法执行,当call方法执行的时候,内部处理了一些事情
- 首先把要操作函数中的this变为call方法第一个传递的实参值
- 把call方法第二个以后的实参(包括第二个)获取到
把要操作的函数执行并且把第二个以后传递进来的实参(包括第二个)传给函数
非严格模式下,如果参数不传,或者第一个传递的是null/undefined,this都指向window
严格模式下,传递的第一个参数是谁this就指向谁,包括null和undefined;不传this就是undefined
返回值:使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined。
//call原理
fn.call(arg1, arg2, ...);
Function.prototype.call=function(){
let param1=arguments[0];
let paramOther=[] ; //把arguments中除了第一个以外的实参获取到
//=> call中的this:fn 当前要操作的函数(函数类的一个实例)
//1、把fn(也就是call中的this)中的this关键字修改为param1 => 把this(call中的this)中的this关键字修改为param1
//把fn(this)执行,paramOther(第二个及以后的参数)传给fn
//this(paramOther)
}
window.name='aaa';
let fn=function (){
console.log(this.name)
}
let obj={
name:'OBJ',
fn:fn
}
let oo={
name:'oo'
}
fn(); //this:window 'aaa'
obj.fn(); // this:obj 'OBJ'
oo.fn() //报错
fn.call(oo) //this:oo 'oo'
fn.call(obj) //this:obj 'OBJ'
手动实现call了解其原理
- 第一步:将函数设为传入对象的属性
- 第二步:执行该函数
- 第三部:删除该函数
Function.prototype.myCall = function(context) {
// 首先要获取调用call的函数,用this可以获取(原型中的this是.前面的函数)
context.fn = this;
context.fn(); //在context上调用函数,那函数的this值就是context.
delete context.fn;
}
接受任意参数有两种思路:
-
处理arguments (原生js)拼出一个字符串,像这样
eval("fn(arguments[1], ...,arguments[n])")然后用eval执行 -
解构运算符 (ES6)
call(target, ...args)
Function.prototype.myCall = function (context, ...args) {
context = context ? Object(context) : window;
// 用this获取调用当前myCall的方法 再绑定到 context 上
context.fn = this
// 获取 传入的参数 (从arguments对象里取)
const val = context.fn(...args);
// 删除 context 上添加的方法
delete context.fn
return val
}
最终代码
Function.prototype.myCall = function(context){
if(context === null || context === undefined){
context = window;
} else {
context = Object(context);
}
let arg = [];
let val ;
// i 从1开始只取第二个和以后的作为参数
for(let i = 1 ; i<arguments.length ; i++){
arg.push( 'arguments[' + i + ']' ) ;
}
// 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]", ...]
context._fn_ = this;
val = eval( 'context._fn_(' + arg + ')' )
delete context._fn_;
return val
}
call.call 原理
一个call是让call前边的函数执行;两个及以上 的call是让call后面括号里的函数执行,如果call后面括号里的不是函数报错
Function.prototype上的call方法(也是一个函数,也是函数类的实例,也可以继续调用call、apply、bind等方法)所以:无论调用几次call实际只最后的执行call.call()
let sum=function(a,b){
console.log(this)
}
let sum2=function(a,b){
console.log(this)
console.log(555)
}
let opt={n:20}
sum.call(opt,20,30);
//浅显 //sum中的this:opt a=20 b=30
//深入 //call执行 call中的this是sum,把this(call中的this也就是sum)中的“this关键字”改为opt
sum.call.call(opt)
//1、sum.call找到Function.prototype上的call方法(也是一个函数,也是函数类的实例,也可以继续调用call、apply、bind等方法) =》A:sum.call
//2、A.call(opt) 继续找到原型上的call方法,把call方法执行:call中的this变为A,A中的this修改为opt然后把A执行(执行的是原型上的call) 报错sum.call.call is not a function
sum.call.call(sum2) //555 this:window
//sum.call => 先找到Function.prototype.call => call.call(sum2)
//在第二个call中 context为sum2、context._fn_ 为call 最终是 sum2.call()
function fn1(){console.log(1)}
function fn2(){console.log(2)}
fn1.call(fn2); //1
//找到callAA并执行,callAA中的this是fn1,fn1中的this为fn2,callAA中执行的是fn1
fn1.call.call(fn2); //2
//fn1.call => 先找到Function.prototype.call => call.call(fn2)
//在第二个call中 context为fn2、context._fn_ 为call 最终是 fn2.call()
//=>让fn2中的this变为undefined,因为fn1.call的时候没有传递参数值,
//然后让fn2执行
Function.prototype.call(fn1) //Function.prototype() 没有输出
Function.prototype.call.call(fn1) //fn1() 1
call继承父级属性用法:父类.call(this,arg1,arg2...)
在一个子构造函数中,你可以通过调用父构造函数的call方法来实现继承
下例中,使用Food和Toy构造函数创建的对象实例都会拥有在Product构造函数中添加的name属性和price属性,但category属性是在各自的构造函数中定义的
function Product(name,price) {
this.name=name;
this.price=price;
}
function Food(name,price) {
Product.call(this,name,price);
this.category='food';
}
function Toy(name,price) {
Product.call(this,name,price);
this.category='toy';
}
var cheese = new Food('aaa',5)
var fun = new Toy('bbb',20)
console.log(cheese.name); //aaa
console.log(fun.price); //20
console.log(cheese.name == fun.name); //false
console.log(cheese.category); //food
console.log(fun.category); //toy
console.log(cheese.category == fun.category); //false
apply:和call基本一样,只有一个区别:传参方式,传递给fn的参数(第一个以后的所有参数)必须放在一个数组中
apply把需要传递给fn的参数(第一个以后的所有参数)放在一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递
fn.call(obj,10,20)
fn.apply(obj,[10,20])
手动实现apply
Function.prototype.myApply = function(context,arr){
if(context === null || context === undefined){
context = window;
} else {
context = Object(obj);
}
let args = [];
let val ;
// i 从0 开始 因为 arr就是参数集合
for(let i = 0 ; i<arr.length ; i++){
args.push( 'arr[' + i + ']' ) ;
}
context._fn_ = this;
val = eval( 'context._fn_(' + args + ')' )
delete context._fn_;
return val
}
bind:语法和call一样,唯一的区别:立即执行还是等待执行调用方法的函数
fn.call(obj,10,20) 改变fn中的this,并让fn立即执行
fn.bind(obj,10,20) 改变fn中的this,fn不执行,必须手动调用fn,fn才会执行(不兼容IE6-8)
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数bind
第一个参数是一个函数, 在执行的时候也是执行的也是bind的第一个参数
bind返回一个函数,该函数执行时使用给定的this执行- bind可以接受参数,在bind缓存arguments,用闭包绑到boundF中,和boundF接受的参数合并(boundF是返回的函数)
注意:返回一个函数,那就是说可以被new,如果被new的话,this不应该指向给定的this,因为如果new的this指向会被改变的话,实例会有问题。
var module = {
x: 42
}
var unboundGetX = function() {
return this.x;
}
var boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42
function fn(a,b){
console.log(this,a,b)
}
var obj={name:'aaa'}
document.onclick=fn; //把fn绑定给点击事件,点击执行
document.onclick=fn(); //在绑定的时候,先把fn执行,把执行的返回值绑定给事件,当点击的时候执行的是undefined
手动实现bind
调用时使用指定this,返回的函数中用call/apply就好了
bind
可以接收两次参数:1、bind时的传参;2、执行时的传参需要将两次调用的传参一起传进去
// 第一版
Function.prototype.myBind = function (context) {
var self = this;
// 获取myBind函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数 (就是 函数执行时的传参)
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
处理new的情况
示例
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
new的表现
- this指向了boundF正在构造的新对象
- boundF的prototype在新对象的原型链上
解决思路
- 判断是不是被new了,是的话把this还给新对象,即判断fBound是不是在新对象(this)的原型链上
- 保证new出来的对象原型链和原函数一致而不是和fBound一致,把原函数的原型链搞到fBound的prototype上即可
// 第二版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
使用apply最终实现
Function.prototype.myBind = function (context) {
// this指向原函数
var that = this
// 取出bind的参数,构造闭包,因为arguments在boundF中会被覆盖访问不到了
var args = Array.prototype.slice.apply(arguments, 1)
// 上边的操作相当于 arguments.slice(1) 区第二个及以后的参数
var boundF = function () {
// 这里的arguments是boundF的arguments, 也就是实际调用时的参数
var bindArgs = Array.prototype.slice.call(arguments)
// 这里的this有两种:
// boundF (普通调用)
// newObj (new)
// 注意boundF instanceof boundF是false的
return that.apply(this instanceof boundF ? this : context, args.concat(bindArgs))
}
// 切换原型链
boundF.prototype = Object.create(this)
return boundF
}
ES6
Function.prototype.myBind = function(context,...arg1){
return (...arg2) => {
let args = arg1.concat(arg2);
let val ;
context._fn_ = this;
val = context._fn_( ...args );
delete context._fn_;
return val
}
}
ES5
Function.prototype.myBind = function(context){
if(context === null || context === undefined){
context = window;
} else {
context = Object(context);
}
let _this = this;
let argArr = [];
for(let i = 1 ; i<arguments.length ; i++){
argArr.push( 'arg1[' + (i - 1) + ']' ) ;
}
return function(){
let val ;
for(let i = 0 ; i<arguments.length ; i++){
argArr.push( 'arguments[' + i + ']' ) ;
}
context._fn_ = _this;
console.log(argArr);
val = eval( 'context._fn_(' + argArr + ')' ) ;
delete context._fn_;
return val
};
}
获取数组中的最大值
let arr=[12,13,14,15,16,23,24,13,12,15]
//=>方法一:从大到小排序,取第一个
let max=arr.sort(function(a,b){
return b-a;
})[0];
//=>方法二:遍历比大小
let max=arr[0]
for(var i=1;i<arr.length;i++){
if(arr[i]>max){
max=arr[i];
}
}
//方法三:基于eval、Math.max;Math.max不能直接把数组传进去;拼成"Math.max(12,13,14...)"
let str="Math.max("+arr.toString()+")";
eval(str);
//方法四:apply的语法:apply把需要传递给fn的参数放在一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递
Math.max.apply(null,arr)
//方法5:利用ES6的展开运算符和Math.max
Math.max(...arr)