1.手动实现new
- 创建一个空对象
- 新对象隐式原型__proto__链接到构造函数显式原型prototype上
- 将步骤 1 新创建的对象作为 this 的上下文 ;( 实际是执行了构造函数 并将构造函数作用域指向新对象 )
- 判断构造函数的返回值类型,如果是值类型,则返回新对象。如果是引用类型,就返回这个引用类型的对象。
function newObject(){
//创建一个空对象
let obj = {};
// 构造函数就是我们传入的第一个参数。
// 由于arguments是类数组,我们不能直接使用shift方法,我们可以使用call来调用Array上的shift方法
let Constructor = [].shift.call(arguments);
//设置新对象的proto属性指向构造函数的原型对象
obj._proto_ = Constructor.prototype;
//让构造函数中的this指向新对象
let result = Constructor.apply(obj,arguments)
//判断构造函数的返回值类型
// obj instanceof Object
return typeof result === 'object' ?result: obj;
}
2.手写instanceof
function myInstanceof(left,right){
let prototype = right.prototype;
left = left._proto_;
while(true){
if(left===null)return false;
if(prototype === left)return true;
left = left._proto_;
}
}
3.手写call
Function.prototype.myCall = function(){
//1.将第一个参数(绑定this的对象)和剩余参数解构出来
let [context,...args] = [...arguments]
context =context || window;
//2.把this(调用_call的方法) 赋值到该对象的一个属性上
context.fn = this
//3.调用对象绑定的方法。
let result = context.fn(...args)
//4.删除绑定的属性
delete context[fn];
//5.返回调用结果
return result;
}
4.手写apply
Function.prototyoe.myApply = function (context,args) {
context = context || window;
context.fn = this;
let result;
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (args) {
if (!Array.isArray(args)) {
throw new Error("参数为数组");
} else {
result = context.fn(...args);
}
} else {
result = context.fn();
}
delete context.fn;
return result;
};
5.手写bind
一些理解:
- arguments
(1)定义:arguments是一个对应于传递给函数的参数的类数组对象
(2)实质是对象,typeof arguments 的结果是object。arguments对象只能在函数内部使用
(3)不是数组,类似于数组,除了length属性和索引元素之外没有任何Array属性。但是可以被转换为一个真正的数组,下图是转化为数组的4种方法
var arg = Array.prototype.slice.call(arguments);
var arg = [].slice.call(arguments);
const arg = Array.from(arguments);
const arg = […arguments];
Array.prototype.concat.apply([], arrayLike);
- [].shift.call(arguments)
[].shift.call(arguments)这条语句的含义是将arguments转化为数组,再对其运用shift方法,得到传入的参数中的第一个参数即this
- args = [].slice.call(arguments);
除了 this 上下文的所有参数,传给了 args ,以备后来使用。
bind 的一些用法
- 绑定this
// 分析:直接调用a的话,this指向的是global或window对象,所以会报错;
// 通过bind或者call方式绑定this至document对象即可正常调用
var a = document.write;
a('hello'); // error
a.bind(document)('hello'); // hello
a.call(document,'hello'); // hello
- 实现预定义参数
// 实现预定义参数
// 分析:Array.prototype.slice.call(arguments)是用来将参数由类数组转换为真正的数组;
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1,2,3]
// 第一个参数undefined表示this的指向,第二个参数10即表示list中真正的第一个参数,依次类推
var a = list.bind(undefined, 10);
var list2 = a(); // [10]
var list3 = a(1, 2, 3); // [10,1,2,3]
- 绑定函数作为构造函数
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + ',' + this.y;
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var YAxisPoint = Point.bind(null, 0/*x*/);
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'
axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(17, 42) instanceof YAxisPoint; // true
//上面例子中Point和YAxisPoint共享原型,因此使用instanceof运算符判断时为true。
如何模拟实现一个 bind 的效果?
-
对于普通函数,绑定this指向
-
对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
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;
}
也可以这么用 Object.create 来处理原型:
Function.prototype.bind = function (context, ...args) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var fbound = function () {
self.apply(this instanceof self ?
this :
context, args.concat(Array.prototype.slice.call(arguments)));
}
fbound.prototype = Object.create(this.prototype);
return fbound;
}
6.手写函数柯里化
定义:柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
- 实现
function currying(fn,...args){
if(fn.length<=args.length){
return fn(...args)
}else{
return (...args2)=>currying(fn,...args,...args2)
}
}
- 测试
function currying(fn,...args){
if(fn.length<=args.length){
return fn(...args)
}else{
return (...args2) =>currying(fn,...args,...args2)
}
}
function sum(a, b, c) {
console.log(a + b + c);
}
const fn = currying(sum);
fn(1,2,3) //6
fn(1,2)(3)//6
fn(1)(2)(3)//6
用途
- 参数复用
function currying(fn,...args){
if(fn.length<=args.length){
return fn(...args)
}else{
return (...args2) =>currying(fn,...args,...args2)
}
}
function getUrl(protocol, domain, path) {
return console.log(protocol + "://" + domain + "/" + path)
}
let fn = currying(getUrl,'http', 'www.conardli.top');
fn('hhh')
- 延迟运行
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
- 函数式编程中,作为compose, functor, monad 等实现的基础
- 提前返回,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法,可以减少判断次数
// 原先的写法
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};
// 柯里化,只走一次判断
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();
性能
- 存取arguments对象通常要比存取命名参数要慢一点
- 一些老版本的浏览器在arguments.length的实现上是相当慢的
- 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
- 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
闭包,函数中的变量都保存在内存中,内存消耗大,有可能导致内存泄漏。
递归,效率非常差,
arguments, 变量存取慢,访问性很差,
练习:
// 实现一个add方法,使计算结果能够满足如下预期:
console.log(add(1)(2)(3) )
console.log(add(1, 2, 3)(4))
console.log(add(1)(2)(3)(4)(5))
function add(){
// 第一次执行时,定义一个数组专门用来存储所有的参数
const _args = [...arguments];
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
const _add = function(...args1){
_args.push(...args1);
return _add;
}
// 利用toString隐式转换的特性 (
// 输出函数名字时 例如 add 等于 调用tostring)
// 当最后执行时隐式转换,并计算最终的值返回
_add.toString = function(){
return _args.reduce((a,b) => a+b)
}
return _add;
}
7.反柯里化
反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,使本来只有特定对象才适用的方法,扩展到更多的对象。(让一个“对象”去借用一个原本不属于他的方法)
//反柯里化1
var uncurrying= function (fn) {
return function () {
var context=[].shift.call(arguments);
return fn.apply(context,arguments);
}
};
//反柯里化2 进阶版
Function.prototype.uncurrying = function() {
var that = this;
return function() {
return Function.prototype.call.apply(that, arguments);
}
};
function sayHi () {
return "Hello " + this.value +" "+[].slice.call(arguments);
}
var sayHiuncurrying=sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));
//反柯里化 进阶版
1 为Function原型添加unCurrying方法,这样所有的function都可以被借用;
2 返回一个借用其它方法的函数,这是目的;
3 借用call方法实现,但call方法参数传入呢?借用apply,至此完毕。
- uncurrying是定义在Function的prototype上的方法,因此对所有的函数都可以使用此方法。调用时候:sayHiuncurrying=sayHi.uncurrying(),所以uncurrying中的 this 指向的是 sayHi 函数; (一般原型-方法中的 this 不是指向原型对象prototype,而是指向调用对象,在这里调用对象是另一个函数,在javascript中函数也是对象)
- call.apply(that, arguments) 把 that 设置为 call 方法的上下文,然后将 arguments 传给 call方法。上例中,that 实际指向 sayHi,所以调用 sayHiuncurrying(arg1, arg2, …) 相当于 sayHi.call(arg1, arg2, …)(apply的作用);
- sayHi.call(arg1, arg2, …), call 函数把 arg1 当做 sayHi的上下文,然后把 arg2,… 等剩下的参数传给 sayHi,因此最后相当于 arg1.sayHi(arg2,…)(call的作用);
- 因此,这相当于 sayHiuncurrying(obj,args) 等于 obj.sayHi(args)。
运用举例2
var test="a,b,c";
console.log(test.split(","));
var split=uncurrying(String.prototype.split); //[ 'a', 'b', 'c' ]
console.log(split(test,',')); //[ 'a', 'b', 'c' ]
split=uncurrying(String.prototyp