JS面试必备之手写call/apply/bind/new
作者:feelingHy
一、Function.prototype.call()
call() 方法用于指定一个 this 值和单独给出的一个或多个参数来调用一个函数。
该方法的语法和作用与 apply() 类似,只有一个区别,就是 call() 方法接受一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
语法:
function.call(thisArg, arg1, arg2, ...)
参数:
- thisArg: 可选的。在 function 函数运行时使用的 this值。请注意,this 可能不是该方法看到的实际值:如果这个函数在非严格模式下,则指定为 null 或
- undefined 时会自动替换为指向全局对象,原始值会被包装。
- arg1,arg2,...: 指定的参数列表。
返回值:
使用调用者提供的的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
描述:
call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
使用:
var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1
这里需要注意两点:
- call 函数改变了 this 的指向,指向了 foo;
- bar 函数执行了;
那我们如何模拟实现呢?我们把代码改造如下:
var foo = { value: 1, bar() { console.log(this.value); } }; foo.bar(); // 1
这个时候 this 指向了 foo,但是却给 foo 对象添加了一个属性,只要思想不滑坡,方法总比困难多,我们用 delete 把它再删除不就好了吗?
1. 把函数设置为对象的属性;
2. 执行函数;
3. 从对象中删除函数;
代码如下:
Function.prototype.call3 = function(context) { // 第一步 context.fn = this; // 第二步 context.fn(); // 第三步 delete context.fn; }
测试代码:
var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call3(foo); // 1
第一步:这里的 context 为 foo,this 为 函数 bar,把 bar 函数赋值为 foo 的一个对象属性,即:foo.fn = bar;
第二步:执行 foo.fn 函数;
第三步:删除 foo.fn 函数;
当然,我们也可以给 call 函数传递参数,函数也可以有返回值,如下:
var value = 'global'; // a var foo = { value: 1 }; function bar(name,age) { console.log(this.value); // b console.log(name); console.log(age); return { age, name }; } bar.call(this, 'tom', 20); // 1 tom 20 var result = bar.call(null, 'tom', 20); // global tom 20 // c console.log(result); // {age: 20, name: "tom"}
this 参数可以传 null,当为 null 的时候,视为指向 window;
函数可以有返回值;
注意:a 处变量的声明要用 var 而不是 let,否则在 c 处调用时,this 为 null 时,b 处的输出结果会是 undefined,这里主要是在全局声明时, 与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的则会)。
实现代码如下:
Function.prototype.call3 = function(context) { context = context || window; let args = [...arguments].slice(1); // 第一步 context.fn = this; // 第二步 let result = context.fn(...args); // 第三步 delete context.fn; return result; }
测试代码如下:
Function.prototype.call3 = function(context) { context = context || window; let args = [...arguments].slice(1); // 第一步 context.fn = this; // 第二步 let result = context.fn(...args); // 第三步 delete context.fn; return result; }
二、Function.prototype.apply()
apply() 方法抵用一个具有给定 this 值的函数,以及一个数组(或一个类数组对象)的形式提供的参数。
语法:
pply(thisArg)
apply(thisArg, argsArray)
参数:
- thisArg: 在 func 函数运行时使用的 this 值,请注意,this 可能不是该方法看到的实际值:如果这个函数在非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
- argsArray: 可选的,一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为null 或者 undefined,则表示不需要传递任何参数。
返回值:
调用有指定 this 值和参数的函数的结果。
描述:
apply 与 call()非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或数组对象,如 fun.apply(this, new Array('eat', 'bananas'))。
你也可以使用 arguments 对象作为 argsArray 参数。arguments 是一个函数的局部变量。它可以被用作被调用对象的所有未指定的参数。这样,你在使用 apply 函数的时候就不需要知道被调用对象的所有参数。你可以使用 arguments 来把所有的参数传递给被调用对象。被调用对象接下来就负责处理这些参数。
备注:虽然这个函数的语法与 call() 几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组。
apply 的实现其实跟 call 差不多,话不多说,直接上代码:
Function.prototype.apply2 = function(context) { context = context || window; let args = [...arguments][1]; // 注意,这里call 传递的参数是一个数组,直接取数组下标第二位就可以了 context.fn = this; let result = context.fn(...args); delete context.fn; return result; }
测试代码:
var value = 'global'; var foo = { value: 1 }; function bar(name,age) { console.log(this.value); console.log(name); console.log(age); return { name, age, }; } var result = bar.apply2(foo, ['tom', 20]); // 1 tom 20 var result2 = bar.apply2(null, ['tom', 20]); // global tom 20 console.log(result); // {age: 20, name: "tom"}
三、Function.prototype.bind()
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
- thisArg: 调用绑定函数时作为 this 参数传递给目标函数的值。如果使用 new 运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 Object。如果 bind 函数的参数列表为空,或者 thisArg 是 null或 undefined,执行作用域的 this 将被视为新函数的 thisArg。
- arg1, arg2, ...: 当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
返回值:
返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
描述:
bind() 函数会创建一个新的绑定函数(bound function,BF)。绑定函数是一个 exotic function object(怪异函数),它包装了元函数对象。调用绑定函数通常会导致执行包装函数。绑定函数具有如下内部属性:
- [[BoundTargetFunction]] - 包装的函数对象。
- [[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
- [[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
- [[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。
绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。
使用一(创建绑定函数):
bind 最简单的用法就是创建一个函数,不论怎么调用,这个函数都有同样的 this 值。JavaScript 新手经常犯的一个错误就是将一个方法从对象中拿出来,然后再调用,期望方法中的 this 是原来的对象。如果不做特殊处理的话,一般会丢失原来的对象。基于这个函数,用原始的对象创建一个绑定函数,巧妙的解决了这个问题。
this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象 var module = { x: 81, getX: function() { return this.x; } }; module.getX(); // 81 var retrieveX = module.getX; retrieveX(); // 返回 9 - 因为函数是在全局作用域中调用的 // 创建一个新函数,把 'this' 绑定到 module 对象 // 新手可能会将全局变量 x 与 module 的属性 x 混淆 var boundGetX = retrieveX.bind(module); boundGetX(); // 81
关于制定 this 的指向,我们可以使用 call 或者 apply 来实现,关于 call 和 apply 的实现,我们在上面已经讲述过了。
模拟实现一:
Function.prototype.bind1 = function(context) { let that = this; return function() { return that.apply(context); } }
测试代码: this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象 var module = { x: 81, getX: function() { return this.x; } }; var boundGetX = retrieveX.bind1(module); var result = boundGetX(); // 81 console.log(result);
使用二(传参数):
bind() 的另一个简单的使用方法是使一个函数拥有预设的初始参数,只要这些参数(如果有的话)作为 bind() 的参数写在 this 的后面,当绑定函数被调用的时候,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数,绑定函数的参数会跟在他们后面。
function list() { return Array.prototype.slice.call(arguments); } function addArguments(arg1, arg2) { return arg1 + arg2 } var list1 = list(1, 2, 3); // [1, 2, 3] var result1 = addArguments(1, 2); // 3 // 创建一个函数,它拥有预设参数列表。 var leadingThirtysevenList = list.bind(null, 37); // 创建一个函数,它拥有预设的第一个参数 var addThirtySeven = addArguments.bind(null, 37); var list2 = leadingThirtysevenList(); // [37] var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3] var result2 = addThirtySeven(5); // 37 + 5 = 42 var result3 = addThirtySeven(5, 10); // 37 + 5 = 42,第二个参数被忽略
接下来我们来实现传参的模拟:
Function.prototype.bind1 = function(context) { let that = this; // 获取 bind1 函数从第二个到最后一个参数 let args = [...arguments].slice(1); return function() { // 这个时候 arguments 是指 bind 返回的函数的入参 return that.apply(context,[...args].concat([...arguments])); } }
测试代码:
var foo = { value: 1 }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); return this.value; } var bindFoo = bar.bind1(foo,'daisy'); bindFoo('18'); // 1
使用三(new 构造函数)
bind 还有一个特点就是,一个绑定函数也能使用 new 操作符创建对象,这种行为就像把原函数当做构造器,提供的 this 将被忽略,同时调用时的参数被提供给模拟函数。
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
尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefined,说明绑定的 this 值失效了,在下面将会见到 new 的模拟实现,就会知道这个时候 this 已经指向了 obj。
下面我们来模拟实现一下:
Function.prototype.bind2 = function(context) { let that = this; // let args = [...arguments].slice(1); let args = Array.prototype.slice.call(arguments,1); var fBound = function() { let bindArgs = Array.prototype.slice.call(arguments); // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 为 true return that.apply(this instanceof fBound ? this : context,args.concat(bindArgs)); }; // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数中的原型 fBound.prototype = this.prototype; return fBound; }
测试代码如下:
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.bind2(foo, 'daisy'); var obj = new bindFoo('18'); // undefined // daisy // 18 console.log(obj.habit); console.log(obj.friend);
上面的 fBound.prototype = this.prototype 有一个缺点,直接修改 fBound.prototype 的时候,也会修改 this.prototype,因为他们是引用同一个地址。
如下:
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.bind2(foo, 'Jack'); // bind2 var obj = new bindFoo(20); // 返回正确 // undefined // Jack // 20 obj.habit; // 返回正确 // shopping obj.friend; // 返回正确 // kevin obj.__proto__.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回错误,这里被修改了 // Kitty
解决方案就是使用一个空对象作为中介,把 fBound.prototype 赋值为空对象的实例。
var fNOP = function () {}; // 创建一个空对象 fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型 fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype
最终实现效果如下:
Function.prototype.bind2 = function(context) { let that = this; // let args = [...arguments].slice(1); let args = Array.prototype.slice.call(arguments,1); var fNOP = function () {}; var fBound = function() { let bindArgs = Array.prototype.slice.call(arguments); // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 为 true return that.apply(this instanceof fBound ? this : context,args.concat(bindArgs)); }; // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数中的原型 fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }
四、new 操作符
使用 new 操作符实例化一个类等于使用 new 调用其构造函数。唯一可感知的不同之处就是,JavaScript 解释器知道使用 new 和类意味着该使用 construct 函数进行实例化。
使用 new 调用类的构造函数会执行如下操作:
- 在内存中创建一个新的对象;
- 改变新对象 proto 指向 构造函数的 prototype 属性;
- 构造函数内部的 this 被赋值为这个新对象;
- 执行构造函数内部的代码;
- 如果构造函数返回 非空对象,则返回该对象,否则,返回刚才创建的新对象。
new 的实现一:
function newCreate() { let obj = new Object(); Constructor = Array.prototype.shift.call(arguments); obj.__proto__ = Constructor.prototype; Constructor.apply(obj, arguments); return obj; }
在这里我们做了以下事情:
- 创建一个新的对象;
- 取出第一个参数,这就是我们要传入的构造函数,因为 shift 会修改原数组,所以 arguments 会被去除第一个参数;
- 将 obj 的 proto 指向构造函数的 prototype;,这样 obj 就可以访问到构造函数原型中的属性;
- 使用 apply 改变构造函数 this 的指向,这样 obj 就可以访问到构造函数中的属性;
- 返回 obj;
测试代码如下:
function Person(name,age) { this.name = name; this.age = age; } Person.prototype.printName = function() { console.log(this.name); }
但是我们还需要注意,如果构造函数返回非空对象,则返回该对象,否则,返回刚才创建的新对象。那么这句话怎么理解呢?请看下面的示例:
function Person(name,age) { this.name = name; this.age = age; return { name, gender: 'male', } } Person.prototype.printName = function() { console.log(this.name); } let p = new Person('tom', 18); console.log(p.name); // tom console.log(p.age); // undefined
在这个例子中,构造函数返回了一个对象,在实例 p 中就只能访问到返回的对象中的属性,也就是说只能访问到 name 和 gender, age 属性是访问不到的。注意,这里返回的是一个对象,如果返回的是一个基本类型数据呢?
function Person(name,age) { this.name = name; this.age = age; return 20; } Person.prototype.printName = function() { console.log(this.name); } let p = new Person('tom', 18); console.log(p.name); // tom console.log(p.age); // 20
在这里返回的是一个基本类型的数据,尽管有返回值,相当于没有返回。
所以,下面我们需要判断一下构造函数的返回值是基本类型数据还是一个一个对象。如果是一个对象,我们就返回这个对象,如果没有,我们就不做任何处理。
function newCreate() { let obj = new Object(); Constructor = Array.prototype.shift.call(arguments); obj.__proto__ = Constructor.prototype; let result = Constructor.apply(obj, arguments); return typeof result === 'object' ? result : obj; }
测试代码:
返回一个基本类型数据:
function Person(name,age) { this.name = name; this.age = age; return 20; } let p1 = newCreate(Person,'rose',20); console.log(p1.name); // rose console.log(p1.age); // 20 console.log(p1.gender); // undefined
返回一个对象:
function Person(name,age) { this.name = name; this.age = age; return { name, gender: 'male', } } let p1 = newCreate(Person,'rose',20); console.log(p1.name); // rose console.log(p1.age); // undefined console.log(p1.gender); // male
以上就是JS面试必备之手写call/apply/bind/new的详细内容,更多关于JS手写call apply bind new的资料请关注脚本之家其它相关文章!