引言
希望通过以下这几道手写题能够加深对JS基础知识点:this绑定、new、原型继承相关知识的理解。
this的指向
this它代表了一个函数执行上下文中的当前对象,this的值取决于函数是如何被调用的,而不是如何被定义的。
【函数调用模式】由于foo是直接调用的,所以this绑定的是全局对象。
var a = 1
function foo(){
//"use strict";
var b = 2
console.log(this.b);
}
foo()
【方法调用模式】而通过obj.sayHello的方式调用方法时,函数调用中的this会绑定到对象obj上。此时name打印出来的是Alice。
let obj = {
name: 'Alice',
sayHello: function() {
console.log(this.name);
}
}
obj.sayHello()
【构造器调用模式】如果一个函数用new调用时,函数执行前会创建一个新对象,this指向这个新创建的对象。
function foo(){
var b = 2
console.log('b', this);
}
new foo()
【apply、call和bind调用模式】这三个方法都可以显示的指定调用函数的 this 指向。
当我们了解了this指向的相关知识后,我们需要了解call、apply、bind内部是如何实现this的重新绑定的,下面的call、apply、bind的手写题目。
1.call
js中call方法可以设置this的绑定,实现代码如下:核心思路是将this指针赋值给context.fn然后通过context.fn()来执行方法,这样子就将函数的this绑定到context对象上。
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError("Not a function")
}
// 不传参数默认是window
context = context || window
// 保存this
context.fn = this
// Array.from 把伪数组转为数组,然后调用slice去掉第一个参数
let args = Array.from(arguments).slice(1)
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2
2.apply
和call差不多,只是获取参数的方式不一样
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError("Not a function")
}
// 不传参数默认是window
context = context || window
// 保存this
context.fn = this
console.log('arguments', ...arguments)
// 调用函数
let result = context.fn(...arguments[1])
delete context.fn
return result
}
function foo(b, c) {
console.log("my call", this.a, b, c) // 2 b c
}
var obj = {
a: 2
}
foo.myApply(obj, ["b", "c"])
3.bind
Function.prototype.myBind = function(context) {
// this指的是需要进行绑定的函数本身,例如foo
const fn = this
const args = Array.from(arguments).slice(1)
const bindFn = function () {
return fn.apply(
// 如果是对象的方式创建则需要绑定this
this instanceof bindFn ? this : context,
// arguments是myBind返回的函数传入的参数
args.concat(...arguments)
)
}
// 返回的绑定函数的原型对象改为原函数的原型对象,即可满足原来的继承关系
bindFn.prototype = Object.create(fn.prototype)
return bindFn
}
function foo(b, c) {
console.log("my call",this, this.a, b, c)
}
var obj = {
a: 2
}
foo.prototype.sayHi = function() {
console.log('hello')
}
const fn = foo.myBind(obj)
fn("bind:1", "bind:2") // my call {a: 2} 2 bind:1 bind:2
const a = new fn("bind:3", "bind:4") // my call bindFn {} undefined bind:3 bind:4
a.sayHi() // hello
可以看到直接通过方法调用fn()/通过new调用(new fn())两种方式得到的this是完全不同的。通过new调用this指向的是这个新创建的对象,而不是传入的context,所以this打印出来的是bindFn{}。
构造函数and原型and原型链
构造函数是用来生成特定类型对象的模版,通常使用构造函数来新建一个对象。
原型对象是存放该类型所有实例共享属性和方法的地方。
构造函数通过其prototype属性链接到原型对象,而新创建的实例则通过内部的__proto__/Object.getPrototype(obj)链接到构造函数的原型对象上,形成原型链。
当访问一个对象的时候,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性。而原型对象又会有自己的原型,这就是原型链。
constructor、prototype和__proto__的关系?
构造函数的prototype指向原型对象。原型对象的constructor指向构造函数。实例对象的__proto__指向原型对象。
现在我们已经了解原型对象、原型链、构造函数的含义,我们可以接着来看看js中的new和Object.create具体都做了什么。
1.new
用途:根据传入的构造函数,创建一个指向构造函数原型的对象。注意处理构造函数的this绑定。
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {}
// 使得空对象的隐式原型指向原函数的显示原型
obj.__proto__ = fn.prototype
// this 指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}
function foo(b) {
console.log(this, b) // foo{}, b
}
foo.prototype.sayHi = function() {
console.log('hello')
}
myNew(foo, 'b')
2.原型式继承(Object.create)
用途:根据传入的原型对象o新建返回一个继承自o的实例对象。
创建一个方法,把方法的原型改成传入的原型对象,然后再用new新建一个方法对象。
function myobject(o) {
function F() {} // 创建一个方法
F.prototype = o // 把方法的原型对象改成o
return new F() // 通过new返回一个原型对象
}
let father = function() {}
father.prototype.getName = function() {
console.log('minor')
}
let son = myobject(father)
son.prototype.getName()
其他高频JS手写题
1.通用数据类型判断
function getType(obj) {
let type = typeof obj
if (type !== "object") { // 先用typeof判断基础类型数据
return type
}
// 对象类型再调用toString进行判断,正则返回最后的类型
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
}
console.log('gettype []:', getType([])) // gettype []: Array
2.模拟instanceOf(LO, RF)
function myInstance(L, R) {
let RP = R.prototype // 取R的显式原型
let LP = L.__proto__ // 取L的隐式原型
while(true) {
if (LP === null) return false
if (RP === LP) return true
LP = LP.__proto__
}
}
3.lodash.get(obj, path, default)
function _get(obj, path, defaultValue) {
// 匹配[]""''.这几个字符之外的任意字符 例如a[0].b.c匹配后 返回[ 'a', '0', 'b', 'c' ]
const result = path.match(/[^\[\]""''.]+/g).filter(Boolean)
.reduce((o, k) => o === undefined ? o : o[k], obj);
return result === undefined ? defaultValue : result;
}
// 使用示例
const obj = { a: [{ b: { c: 3 } }] };
console.log(_get(obj, 'a[0].b.c', 'default')); // 输出: 3
console.log(_get(obj, 'a[0].b.d', 'default')); // 输出: 'default'
console.log(_get(obj, 'a[0]["b"]["c"]', 'default')); // 输出: 3
4.deepClone
function deepClone(startObj) {
const obj = Array.isArray(startObj) ? [] : {}
for (let key in startObj) {
if (typeof startObj[key] === 'object') {
obj[key] = deepClone(startObj[key])
} else {
obj[key] = startObj[key]
}
}
return obj
}
function test() {
const a = {
b : { c: 1},
d : [1, 2]
}
const b = [1, 2, a ]
console.log(deepClone(a), deepClone(b))
}
5.debounce,throllte
/ 单位时间内频繁触发一个事件,以最后一次触发为准
function debounce(func, delay) {
let timer
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
func.call(this)
}, delay)
}
}
// 单位时间内频繁触发一个事件,只触发一次
function throllte(func, delay) {
let timer
return function() {
if (timer) return
timer = setTimeout(() => {
func.call(this)
timer = null
}, delay)
}
}