js手写题

1.手动实现new

  • 创建一个空对象
  • 让构造函数中的this指向新对象
  • 设置新对象的proto属性指向构造函数的原型对象
  • 判断构造函数的返回值类型,如果是值类型,则返回新对象。如果是引用类型,就返回这个引用类型的对象。
function newObject(){
	//创建一个空对象
	let obj = {};
	// 排除第一个构造函数参数
	Constructor = [].shift.call(arguments);
	//设置新对象的proto属性指向构造函数的原型对象
	obj._proto_ = Constructor.prototype;
	//让构造函数中的this指向新对象
	let result = Constructor.apply(obj,arguments)
	//判断构造函数的返回值类型
	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);
Array.prototype.splice.call(arrayLike, 0);

  • [].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.myBind(context,...args){
	// ...args用于保存绑定bind时传入的剩余参数(除去第一个 即this)
 	// 异常处理
	if(this !=='function'){
		throw new TypeError('error')
	}
	// 保存this的值,它代表调用 bind 的函数
	var self = this;
	var fNop = function(){};
	var fbound = function (){
		self.apply(this instanceof self ?
							this:
							context,args.concat(  [].slice.call(arguments) )
						)
	}
	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 = 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, 变量存取慢,访问性很差,

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.prototype.split) 给 uncurrying 传入一个具体的fn,即String.prototype.split ,split 函数就具有了 String.prototype.split 的功能,函数调用 split(test,’,’) 时,传入的第一个参数为 split 执行的上下文,剩下的参数相当于传给原 String.prototype.split 函数。

var split=uncurrying(String.prototype.split);
split(test,’,’);
1.传入 String.prototype.split 给fn, fn 被应用为 Function.prototype.call 的上下文,然后封装在一个新函数里面返回;
2.返回新函数后给 split,调用时 split(test,’,’),则 arguments 为 [test,’,’]
3.接下来由于闭包特定 保存了 fn , apply 到call,相当于fn.call(arguments),就是例子中的Function.prototype.split.call(test,’,’)`
4.因此第一个参数 test 对象被设置为 Function.prototype.split 的上下文,其余参数 ‘,’ 传给 split 函数
5.由此可见 Function.prototype.call.apply(fn,arguments) 相对于 fn.apply(arguments[0],args) 应用了 call 之后,相当于自动将 arguments 分拆成第一个参数和剩下的参数,并分别应用。 省去了前面两种写法要分拆第一个参数的步骤。

参考:
详解JS函数柯里化
浅析函数柯里化与反柯里化
前端开发者进阶之函数反柯里化unCurrying

8.数组扁平化

实现方法:

  • 手写循环
function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        }
        else {
            result.push(arr[i])
        }
    }
    return result;
}

  • ES6中的flat方法
ary = arr.flat(Infinity);
  • replace + split (利用正则)
let str = JSON.stringify(ary);
let arr = str.replace( /(\[|\])/g,  '').split(',')
//最后数组中的元素为字符串 
  • 扩展运算符(利用some循环 while 和扩展运算符)

function flatten(arr) {

    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }

    return arr;
}
  • 手写数组扁平化reduce
function flatten(ary) {
    return ary.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}

手写数组扁平化–指定扁平化深度d

function flatDeep(arr, d = 1) {
  return d > 0
    ? arr.reduce(
        (acc, val) =>
          acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val),
        []
      )
    : arr.slice();
}

9.实现一个事件委托

<ul id="ulnode">
        下面的内容是子元素1
        <li>li内容>>> <span> 这是span内容123</span></li>
        下面的内容是子元素2
        <li>li内容>>> <span> 这是span内容123</span></li>
        下面的内容是子元素3
        <li>li内容>>> <span> 这是span内容123</span></li>
    </ul>
    <script>
        var ul = document.getElementsByTagName("ul")[0];
        function delegate(element, eventType, selector, fn) {
            element.addEventListener(
                eventType,
                (e) => {
                    let el = e.target;
                    //console.log(el)
                    while (!el.matches(selector)) {
                        console.log(el.matches(selector))
                        console.log(element)
                        if (element === el) {
                            el = null;
                            break;
                        }
                        console.log(el)
                        el = el.parentNode;
                    }
                    el && fn.call(el, e, el);
                },
                true
            );
            return element;
        }

        delegate(ul, "click", "li", function () {
            console.log(this.innerText);
        });

点击span时效果:
在这里插入图片描述

10.数组乱序

从最后一个元素开始,从数组中随机选出一个位置,交换,直到第一个元素。

function disorder(array) {
      const length = array.length;
      let current = length - 1;
      let random;
      while (current >-1) {
        random = Math.floor(length * Math.random());
        [array[current], array[random]] = [array[random], array[current]];
        current--;
      }
      return array;
    }

11.手写promise

function MyPromise(executor) {
      this.state = PENDING;
      this.value = null;
      this.reason = null;
      this.onFulfilledCallbacks = [];
      this.onRejectedCallbacks = [];

      const resolve = (value) => {
        if (this.state === PENDING) {
          this.state = FULFILLED;
          this.value = value;
          this.onFulfilledCallbacks.forEach(fun => {
            fun();
          });
        }
      }

      const reject = (reason) => {
        if (this.state === PENDING) {
          this.state = REJECTED;
          this.reason = reason;
          this.onRejectedCallbacks.forEach(fun => {
            fun();
          });
        }
      }

      try {
        executor(resolve, reject);
      } catch (reason) {
        reject(reason);
      }
    }

then方法异步调用

 MyPromise.prototype.then = function (onFulfilled, onRejected) {
      if (typeof onFulfilled != 'function') {
        onFulfilled = function (value) {
          return value;
        }
      }
      if (typeof onRejected != 'function') {
        onRejected = function (reason) {
          throw reason;
        }
      }
      const promise2 = new MyPromise((resolve, reject) => {
        switch (this.state) {
          case FULFILLED:
            setTimeout(() => {
              try {
                const x = onFulfilled(this.value);
                resolve(x);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
            break;
          case REJECTED:
            setTimeout(() => {
              try {
                const x = onRejected(this.reason);
                resolve(x);
              } catch (reason) {
                reject(reason);
              }
            }, 0);
            break;
          case PENDING:
            this.onFulfilledCallbacks.push(() => {
              setTimeout(() => {
                try {
                  const x = onFulfilled(this.value);
                  resolve(x);
                } catch (reason) {
                  reject(reason);
                }
              }, 0);
            })
            this.onRejectedCallbacks.push(() => {
              setTimeout(() => {
                try {
                  const x = onRejected(this.reason);
                  resolve(x);
                } catch (reason) {
                  reject(reason);
                }
              }, 0);
            })
            break;
        }
      })
      return promise2;
    }

12.手写单例模式

  • 在合适的时候才创建对象(惰性),并且只创建唯一的一个。

  • 创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。

使用闭包实现:

var Singleton = function(name) {
    this.name = name;
};

Singleton.prototype.getName = function() {
    alert(this.name);
};

Singleton.getInstance = (function(name) {
    var instance;
    return function(name){
        if (!instance) {
            instance = new Singleton(name);
        }
        return instance;
    }
})();

var a = Singleton.getInstance('ConardLi');
var b = Singleton.getInstance('ConardLi2');

console.log(a===b);   //true

上述代码中, Singleton类挂载了一个静态方法 getInstance,如果要获取实例对象只能通过这个方法拿,这个方法会检测是不是有现存的实例对象,如果有就返回,没有就新建一个。

设计模式之单例模式:如何成为你的“唯一”

13.手写jsonp

    // 手动实现jsonp跨域
(function (window,document) {
    "use strict";
    var jsonp = function (url,data,callback) {

        // 1.将传入的data数据转化为url字符串形式
        // {id:1,name:'jack'} => id=1&name=jack
        var dataString = url.indexof('?') == -1? '?': '&';
        for(var key in data){
            dataString += key + '=' + data[key] + '&';
        };

        // 2 处理url中的回调函数
        // cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
        var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
        dataString += 'callback=' + cbFuncName;

        // 3.创建一个script标签并插入到页面中
        var scriptEle = document.createElement('script');
        scriptEle.src = url + dataString;

        // 4.挂载回调函数
        window[cbFuncName] = function (data) {
            callback(data);
            // 处理完回调函数的数据之后,删除jsonp的script标签
            document.body.removeChild(scriptEle);
        }

        document.body.appendChild(scriptEle);
    }

    window.$jsonp = jsonp;

})(window,document)

参考:
手动实现jsonp

14.异步循环打印

如何解决下面的循环输出问题?

for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}
//法1
for(var i=1;i<=5;i++){
	setTimeout(function timer(j){
		console.log(j)
	},0,i)
}
//法2
for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}

法3
for(let i = 1; i <= 5; i++){
  setTimeout(function timer(){
    console.log(i)
  },0)
}


异步循环打印


var sleep = function (time, i) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(i);
    }, time);
  })
};
var start = async function () {
  for (let i = 0; i < 6; i++) {
    let result = await sleep(1000, i);
    console.log(result);
  }
};
start();

15.数组去重

Object

const unique = (array)=> {
    var container = {};
    return array.filter((item, index) =>  container.hasOwnProperty(item) ? false : (container[item] = true));
}

indexOf + filter

const unique = arr => arr.filter((e,i) => arr.indexOf(e) === i);

Set

const unique = arr => Array.from(new Set(arr));
const unique = arr => [...new Set(arr)];

排序

通过比较相邻数字是否重复,将排序后的数组进行去重。
 const unique = (array) => {
      array.sort((a, b) => a - b);
      let pre = 0;
      const result = [];
      for (let i = 0; i < array.length; i++) {
        if (!i || array[i] != array[pre]) {
          result.push(array[i]);
        }
        pre = i;
      }
      return result;
    }

去除重复的值

const filterNonUnique = arr => arr.filter(i => 
  arr.indexOf(i) === arr.lastIndexOf(i)
)

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

var singleNumbers = function(nums) {
    let sum=0;
    for(let i=0;i<nums.length;i++){
        sum^=nums[i]
    }
    let h=1;
    while((sum&h)==0){
        h<<=1
    }
    let num1 = 0;
    let num2 = 0;
    for(let item of nums){
        if((item&h)===0){
            num1^=item
        }else{
            num2^=item
        }
    }
    return [num1,num2]
};

猜你喜欢

转载自blog.csdn.net/HZ___ZH/article/details/112726148