this/原型 原型链/深浅拷贝

585 阅读6分钟

this

参考链接: 嗨,你真的懂this吗?

this是什么?

this关键字总是指向函数所在的当前对象。ES6又新增链另一个类似的关键字super。指向当前对象的原型对象。

一句话解释this是什么? this总是指向调用函数的对象

this的绑定规则

寻找函数this指向的是谁?

  1. 判断是否通过new的方式调用,如果是,那么this就是指向新创建的对象
  2. 判断是否通过bind、call、apply调用,如果是,那么this就是指向bind、call、apply后的调用的第一个参数
  3. 判断是否是隐式调用obj.foo(),如果是,this指向obj
  4. 如果以上都不是,就是普通函数调用foo(),this指向window
  5. 如果是箭头函数,this指向包裹箭头函数的第一个普通函数中的this

绑定优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

默认绑定

    function foo() {
        console.log(this.x);
    }
    var x = 'test';
    fn(); //test

对于这种直接调用一个函数foo()来说,就算默认绑定,this指向window

隐式绑定

    function foo() {
        console.log(this.x);
    }
    var x = 'test';
    const obj = {
        x: 1,
        foo: foo
    }
    obj.foo(); //1

隐式绑定会把函数调用中的this(即此例foo函数中的this)绑定到这个上下文对象(即此例中的obj) 对于obj.foo()来说,谁调用了函数foo,this就指向谁(obj)

显式绑定

显式绑定指的是bind、call、apply这种方式调用

call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。

function foo() {
    console.log(this.x);
}
const obj = {
    x: 1,
    foo: foo
}
var x = 'test';
foo.call(obj); //1
foo.call(null) //test

如果第一个参数是null或者undefined,相当于默认绑定规则

new绑定

使用new来调用函数,会自动执行下面的操作:

  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象,即this指向这个新对象
  3. 执行构造函数中的代码
  4. 返回新对象
function foo(x) {
    this.x = x
}
var fn = new foo('aaa');
console.log(fn.x); //aaa

对于new的方式来说,this被永远绑定在了新对象fn上。但是前提是构造函数中没有返回对象或者是function,否则this指向这个对象或者是function

箭头函数

function foo() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
console.log(foo()()())

对于箭头函数来说,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this. 因为包裹箭头函数的第一个普通函数是foo,所以此时的 this 是 window。

箭头函数使用,注意

  1. 函数体内的this对象,继承的是外层代码块的this。
  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
  5. 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向。

原型/构造函数/原型链

原型

在JavaScript中,每当定义一个对象时候,对象中都会包含一些预定义的属性。其中每个对象都有一个__proto__属性,这个属性指向了原型对象。 使用原型对象的好处是所有对象实例共享它包含的属性和方法

构造函数

在上面的图中还可以发现一个constructor属性,也就是构造函数

打开构造函数,里面还有一个prototype属性,这个属性对应的值和在__proto__中的一样。

obj.__ proto __= obj. __ proto __.constructor.prototype

结论:原型的constructor属性指向构造函数,构造函数又通过prototype属性指回原型。

注意:并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性。

原型链

原型链主要是解决继承的问题

每个对象都拥有一个原型对象,通过__proto__指向其原型对象,并从中继承它的属性和方法,同时,原型对象也有可能有原型,这样一层一层的,最终指向null(Object.prototype.__proto __指向的是null).这种关系称为原型链。

原型/构造函数/原型链直接的关系

定义一个构造函数Foo
new一个实例对象fn
    function Foo() {
        console.log(1)
    }
    let fn = new Foo(); // 当new一个实例对象 fn就会有一个__proto__属性,指向原型对象
    
    fn.__proto__指向原型对象
    fn.__proto__.constructor = Foo 
    fn.__proto__.constructor.prototype = Foo.prototype 
    fn.__proto__ = fn.__proto__.constructor.prototype 
    fn.__proto__ = Foo.prototype //true
    实例fn的__proto__与构造函数的prototype指向同一个对象
    原型对象 等于 构造函数的prototype
    

深浅拷贝

我们了解到对象(引用)类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。通常在开发中我们不希望出现这样的问题。

let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2  希望是1
let a = {
  name: 'xxx',
  obj: {
    aa: 3
    }
}
let b = a
a.obj.aa = 5
console.log(b.obj.aa) // 5  希望是3

浅拷贝

如果我们要复制对象的所有属性都不是引用类型时,就可以使用浅拷贝,实现方式就是遍历并复制,最后返回新的对象。

function shallowCopy(obj) {
    var copy = {};
    // 只复制可遍历的属性
    for (key in obj) {
        // 只复制本身拥有的属性
        if (obj.hasOwnProperty(key)) {
            copy[key] = obj[key];
        }
    }
    return copy;
}
let a = {
  age: 1
}
let b = shallowCopy(a)
a.age = 2
console.log(b.age) // 1

JS内部实现了浅拷贝,如Object.assign(target, source),其中第一个参数是我们最终复制的目标对象,后面的所有参数是我们的即将复制的源对象,支持对象或数组,一般调用的方式为 var newObj = Object.assign({}, originObj);

深拷贝

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

简单实现

function cloneDeep(source) {
    var target = {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (typeof source[key] === 'object') { //问题一  typeof null === 'object' 传入null 会返回{} 应该返回 null
            // 问题二 数组会转成对象 [1,2] 会变成 {0: 1, 1: 2}
                target[key] = cloneDeep(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
let a = {
  name: 'xxx',
  obj: {
    aa: 3
    }
}
let b = cloneDeep(a)
a.obj.aa = 5
console.log(b.obj.aa) // 3

一个简单的深拷贝就完成了,但是这个实现还存在很多问题。

  • 1、因为 typeof null === 'object',传入 null 时应该返回 null 而不是 {}
  • 2、没有考虑数组的兼容, 传入 [1,2 ]时 应该返回 [1,2] 而不是 {0:1, 1:2}

解决以上两个问题

function cloneDeep2(source) {
 
    if (!isObject(source)) return source; // 非对象返回自身
    // if(source === null) return null 
    
    //解决数组兼容
    var target = Array.isArray(source) ? [] : {};
    
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep2(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
 
 
// 解决typeof null === 'object'
function isObject(obj) {
    return typeof obj === 'object' && obj != null;
}

更全面一些

function deepClone(source) { //递归拷贝
    if(source === null) return null; //null 的情况
    if(source instanceof RegExp) return new RegExp(source);
    if(source instanceof Date) return new Date(source);
    if(typeof source !== 'object') {
        //如果不是复杂数据类型,直接返回
        return source;
    }
    //解决数组兼容
    var target = Array.isArray(source) ? [] : {};
 
    for(let key in source) {
        //如果 source[key] 是复杂数据类型,递归
        target[key] = deepClone(source[key]);
    }
    return target;
}

推荐一个网站js库,已经实现好的一些API,可以直接用 lodash.com/ 其中深拷贝API lodash.com/docs#cloneD…