JS面向对象:new原理、原型和原型链全解

new

普通函数执行

    function Func(x, y) {
        let total = x + y;
        this.res = total
    }

    let f = Func(10, 20);
    console.log(res)    // 30

构造函数执行

函数执行的时候通过 new 函数()叫做构造函数执行

  • 此时这个函数被称为一个自定义类(构造函数)
  • 返回值被称为当前自定义类的一个实例(有特殊情况)
    function Func(x, y) {
        let total = x + y;
        this.res = total
    }

    let f = new Func(10, 20);
    console.log(f)

分析new所做的事

构造函数也具备普通函数执行的规则(形成私有上下文等)

  1. 初始化作用域链
  2. 初始化this: window
  3. 初始化arguments
  4. 形参赋值
  5. 变量提升
  6. 代码执行

new 函数执行的特殊性

  1. 私有上下文形成后的第一步是创建实例对象
  2. 初始化this,让上下文中的this指向创建的实例对象
  3. 代码执行中遇到的this.xxx都是给实例对象设置属性
  4. 即使函数中没有return,也会默认把创建的实例对象返回
    1. 如果有return,return的是基本类型的值,默认返回的还是实例
    2. 如果return的是引用类型值,则以return的为主

在这里插入图片描述
f就是当前Func这个类的一个实例

new 不加小括号时

    function Func(x, y) {
        let total = x + y;
        this.res = total
    }
    let f2 = new Func;
    console.log(f2)

属于不带参数new,依然会执行函数,只是不传递实参
在这里插入图片描述

验证某个属性是否为当前对象的属性

  1. in:无论是私有还是公有,只要是它的属性,结果就为true
  2. hasOwnProperty:只有是私有属性时结果才为true
    在这里插入图片描述

验证是否为类的实例

当手动返回一个对象时,实例将会

    function Func(x, y) {
        let total = x + y;
        this.res = total;
        return {
            name: "test"
        }
    }

    let f = new Func(10, 20);
    console.log(f)

    let f2 = new Func;
    console.log(f2)

    console.log(f2 instanceof Func) // false

原型prototype和原型链__proto__

  1. 每一个类(构造函数)都具备prototype,并且属性值是一个对象,存储当前类供实例调用的公共属性和方法
  2. 在原型对象上具备一个内置属性:constructor,存储的值是当前类本身,所以我们把类称为构造函数
  3. 每一个对象(普通对象、prototype、实例、函数等)都具备:__proto__原型链属性(隐式属性),属性值是当前实例所属类的原型实例.__proto__===所属类.prototype

函数类型

  • 普通函数
  • 构造函数(类)
  • 内置类

对象类型

  • 普通对象、数组、正则、日期…
  • prototype、__proto__
  • 类的实例(排除基本数据类型值的特殊性)
  • 函数也是对象
  • 万物皆对象…

内置类原型链机制

在这里插入图片描述

  • Object作为所有原型对象的基类,他的__proto__只能指向自己,但是没有意义,所以赋值为null

arr1.length 私有
arr1.push() 私有里没有,会通过__porto__找所属类原型上有没有
arr1.hasOwnProperty 所属原型类上也没有,就找基类Object原型对象里找

原型链机制

基于__proto__找
基于__proto__继续往上找到
是否私有
所属类.prototype
Object.prototype为止

例如arr1.push() :

  1. 首先找私有方法,
  2. 没有就通过__proto__往上找Array.prototype上的属性方法,
  3. 找到了,并且让其执行,
  4. 方法中的this是arr1,从而实现给arr1末尾设置新的内容
  5. 等价于Array.prototype.push.call(arr1)或者arr1.__proto__.push.call(arr1)

原型中的属性方法:

  • 相对于实例来说是公有的
  • 相对于自己来说是私有的

自定义类原型机制

  1. 先确定执行的是哪的方法
  2. 方法执确定方法中的this
  3. 执行代码结果确定
function Fn() {
    this.x = 100;
    this.y = 200;
    this.getX = function () {
        console.log(this.x);
    }
}
Fn.prototype.getX = function () {
    console.log(this.x);
};
Fn.prototype.getY = function () {
    console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);	// false
console.log(f1.getY === f2.getY);	// true
console.log(f1.__proto__.getY === Fn.prototype.getY);	// true
console.log(f1.__proto__.getX === f2.getX);	// false
console.log(f1.getX === Fn.prototype.getX);	// false
console.log(f1.constructor);	// 指向类本身 Fn(){}
console.log(Fn.prototype.__proto__.constructor);	// 指向基类本身
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();

类原型的重定向

重定向的问题

  1. 重定向的对象中没有constructor
  2. 原始的原型对象上存放的属性方法不会被放到新对象上,导致实例不能调用
  3. 原始的原型对象不被占用后会被释放

重定向的好处

向内置的原型上扩展方法
添加方法很麻烦

function Func() {
}
Func.prototype.XX = function () {};
Func.prototype.A = function () {};
Func.prototype.B = function () {};

设置别名(治标不治本)

let proto = Func.prototype;
proto.A = function () {};
proto.B = function () {};

向原型上批量设置属性方法都是基于重定向的方式

Func.prototype = {
	A: function () {},
	B: function () {}
};

解决重定向问题

  1. 缺失了constructor
  2. 缺失了原始的原型对象上存放的属性方法
    在这里插入图片描述

方法一:只需要constructor,手动加上

    Func.prototype = {
        constructor: Func,
        A: function () { },
        B: function () { }
    }

方法二:通过Object.assign 合并,新的原型对象替换原始的

但是如果新老有个属性方法相同,则会替换

    Func.prototype.Xxx = function () { };

    Func.prototype = Object.assign(Func.prototype, {
        A: function () { },
        B: function () { }
    })

方法三:把老原型对象作为新原型对象的上级原型

    function Func() {
    }
    Func.prototype.Xxx = function () { };

    let protoNew = Object.create(Func.prototype);
    protoNew = Object.assign(protoNew, {
        A: function () { },
        B: function () { }
    })

    console.log(protoNew)
    Func.prototype = protoNew;
    let f = new Func();
    console.log(f)

在这里插入图片描述

内置new 和 Object.create的实现

手写一个new使得以下代码成立

    function Dog(name) {
        this.name = name;
    }
    Dog.prototype.bark = function () {
        console.log('wangwang');
    }
    Dog.prototype.sayName = function () {
        console.log('my name is ' + this.name);
    }
    function _new(Func, ...args) {};
    let sanmao = _new(Dog, '三毛');
    sanmao.bark(); //=>"wangwang"
    sanmao.sayName(); //=>"my name is 三毛"
    console.log(sanmao instanceof Dog); //=>true

new 的实现

  1. 创建一个FUNC的实力对象
  2. 初始化一个this指向这个实例对象
  3. 分析函数执行的返回值(没有返回值或者返回的是原始值都默认返回创建的实例,否则以函数自身返回的为主)

两个参数

  1. Func 要操作的这个类
  2. args 要传递给Func的参数

创建一个FUNC的实例对象

let obj = {};
obj.__proto__ = Func.prototype;

初始化一个this指向这个实例对象

let result = Func.call(obj, ...args);

分析函数执行的返回值

if (result !== null && /^(object|function)$/.test(typeof result)) {
    return result;
}
return obj;

测试

    function _new(Func, ...args) {
        let obj = {};
        obj.__proto__ = Func.prototype; 
        let result = Func.call(obj, ...args);
        if (result !== null && /^(object|function)$/.test(typeof result)) {
            return result;
        }
        return obj;
    }
    let sanmao = _new(Dog, '三毛');
    sanmao.bark(); //=>"wangwang"
    sanmao.sayName(); //=>"my name is 三毛"
    console.log(sanmao instanceof Dog); //=>true

在这里插入图片描述

考虑兼容性 Object.create()

谷歌实现了原型机制,完成原型查找
但是在IE浏览器中禁止使用__proto__,为了防止改变原型指向

Object.create()

  • Object.create([OBJECT])
    创建一个空对象x,并且把[OBJECT](这个值需要是一个对象)作为新对象的原型指向x.__proto__=[OBJECT]
    在这里插入图片描述

  • Object.create(null):创建一个没有原型和原型链的空对象(不是任何类的实例)

但是低版本浏览器也会有不兼容这个方法的情况

手写Object.create

  • 我们写的这个方法不支持null的处理
  • 创建一个类,让类.prototype等于传递的prototype
    返回这个类的实例,使得实例.__proto__ === 类.prototype
    这样就实现了原型链的改变
Object.create = function create(prototype) {
    if (prototype === null || typeof prototype !== "object") {
        throw new TypeError(`Object prototype may only be an Object: ${prototype}`);
    }
    // 创建一个类,创建这个类的实例,实例.__proto__=类.prototype;而我们让类.prototype等于传递的prototype;
    function Temp() { }
    Temp.prototype = prototype;
    return new Temp;
};

完整代码

Object.create = function create(prototype) {
    if (prototype === null || typeof prototype !== "object") {
        throw new TypeError(`Object prototype may only be an Object: ${prototype}`);
    }

    function Temp() { }
    Temp.prototype = prototype;
    return new Temp;
};
function Dog(name) {
    this.name = name;
}
Dog.prototype.bark = function () {
    console.log('wangwang');
}
Dog.prototype.sayName = function () {
    console.log('my name is ' + this.name);
}

function _new(Func, ...args) {

    // let obj = {};
    // obj.__proto__ = Func.prototype;
    let obj = Object.create(Func.prototype);

    let result = Func.call(obj, ...args);

    if (result !== null && /^(object|function)$/.test(typeof result)) {
        return result;
    }
    return obj;
}
let sanmao = _new(Dog, '三毛');
// let sanmao = new Dog("三毛")
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true

基于内置类原型扩展方法

  • 为了防止自己的方法覆盖内置方法,自己的方法需要加前缀
  • 优势:以后使用方便,和内置方法类似,直接让实例调用
  • 方法中的this一般是当前要操作的实例(不需要基于形参传递实例进来了)
  • 只要保证方法的返回结果还是当前类的实例,那么我们就可以基于链式方法调用当前类中提供的其他方法
    Array.prototype.myDistinct = function () {
        console.log(this);
    }

    let arr = [1, 2, 2, 3, 3, 4];
    arr.myDistinct();

扩展一个数组去重方法

    Array.prototype.myDistinct = function () {
        // console.log(new Set(this));
        let newArr = [...new Set(this)];
        return newArr;
    }

    let arr = [1, 2, 2, 3, 3, 4];
    console.log(arr.myDistinct())

for in 与 for of

  • for in
    遍历对象,所有可被枚举的属性都可以遍历到(大部分私有属性和自己向内置类原型扩展的属性)
Object.prototype.myXx = function () { };
let obj = {
    name: "xxx",
    age: 20
}

for (let key in obj) {
    console.log(key)
}
name
age
myXx

所以使用for in一般会加上一个判断,除去非私有的属性

for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(key)
    }
}

或者使用基类中的方法Object.keys

Object.keys(obj).forEach(key => {
    console.log(key, obj[key])
})

扩展技巧

  1. 可以直接让实例调用,而此时执行方法,方法中的this是调用的实例(调取方便)
  2. 需要注意的是,自己扩展的方法不要覆盖内置的方法(最好带个命名前缀my、_
  3. 可以实现链式写法:上一个方法返回的结果可以调取本结果所属类上的原型方法

原型属性扩展相关题目

let n = 10;
let m = n.plus(10).minus(5);
console.log(m);//=>15(10+10-5)
  • this -> 永远是对象数据类型的值(除了null/undefined)
  • 对象+数字:不一定都是字符串拼接
  • 对象本身是想转换为数字再进行运算的
  • 对象转换为数字,并不是先toString,他需要先调用valueOf()获取原始值[[PrimitiveValue]](原始值是基本类型的值)
    如果有原始值,直接基于原始值处理,没有原始值的才去toString
    在这里插入图片描述
    在这里插入图片描述
    Object.prototype.minus = function (num) {
        return this - num;
    }

    Object.prototype.plus = function (num) {
        return this + num;
    }

考虑不符合数字转换的情况

    function init(num) {
        num = Number(num);
        return isNaN(num) ? 0 : num;
    }

    Object.prototype.minus = function (num) {
        num = init(num);
        return this - num;
    }

    Object.prototype.plus = function (num) {
        num = init(num);
        return this + num;
    }

函数与类与对象

  1. 普通函数(作用域和作用域链)
  2. 构造函数(类、实例、原型和原型链)
  3. 普通对象(键值对)
  4. 所有函数都是类的实例
  5. 函数都是对象,任何一个函数都能调用hasOwnProperty/call/aplly/bind
  6. 但是对象不是函数
typeof Array // "function"
typeof Object // "function"
Object instanceof Function // true
Function.prototype === Function.__proto__ // true

Object作为函数(类)一定是Function 的实例

Object.__proto__ === Function.prototype

Function既是一个类也是一个对象,也是Object的实例

Function.__proto__ === Function.prototype
true
Function.prototype.__proto__ === Object.prototype
true

在这里插入图片描述
在这里插入图片描述

补充:运算符优先级

谁优先级大就先运算谁

  • 成员访问obj.xxx 19
  • 带参数的new new Fun() 19
  • 不带参数的new new Fun 19
new Foo.getName();

先执行成员访问–普通函数执行
然后执行new创建实例

new Foo().getName()

先创建实例,再成员访问执行普通函数Foo实例.getName()

new new Foo().getName()

相当于new 实例.getName() 这时候是无参数new,所以下一步执行成员访问

题目

1

function Foo() {
	// 注意这一步是操作变量,而不是实例属性
    getName = function () {
        console.log(1);
    };
    return this;
}
Foo.getName = function () {
    console.log(2);
};
Foo.prototype.getName = function () {
    console.log(3);
};
var getName = function () {
    console.log(4);
};
function getName() {
    console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

2

    let url = "http://www.zhufengpeixun.cn/?lx=1&from=wx#video";
    console.log(url.queryURLParams("from")); //=>"wx"
    console.log(url.queryURLParams("_HASH")); //=>"video"

解法一,利用URL方法

    String.prototype.queryURLParams = function queryURLParams(type) {
        let obj = {
            // lx: "1",
            // from: "weixin",
            // _HASH: "video"
        };
        let { search, hash } = new URL(this);
        // 处理HASH
        if (hash) {
            obj["_HASH"] = hash.substring(1);
        }

        // params
        if (search) {
            search = search.substring(1).split("&");
            // [key=value, key=value]
            search.forEach(item => {
                let [key, value] = item.split("=");
                obj[key] = value;
            })
            // [[key, value], [key, value]]


        }
        console.log(obj)
    }

解法二:正则处理

String.prototype.queryURLParams = function queryURLParams(key) {
    let obj = {};
    this.replace(/([^?=&#]+)=([^?=&#]+)/g, (_, key, value) => obj[key] = value);
    this.replace(/#([^?=&#]+)/g, (_,hash) => obj["_HASH"] = hash)
    return key ? obj[key] : obj;
}

let url = "http://www.zhufengpeixun.cn/?lx=1&from=wx#video";
console.log(url.queryURLParams("from")); //=>"wx"
console.log(url.queryURLParams("_HASH")); //=>"video"
console.log(url.queryURLParams());

面向对象的变相运用

push的原理

题目

let obj = {
    2: 3,
    3: 4,
    length: 2,
    push: Array.prototype.push
}
obj.push(1);
obj.push(2);
console.log(obj);

先理解 push所要做的事情

Array.prototype.push = function push(item) {
    this //-> 需要处理的实例
    // 数组长度需要加一,最后一项是this.length-1,加一项就是length - 1 + 1
    this[this.length] = item;
    // 给改变length属性
    this.length++;
    return this.length;
};

所以分析结果

    let obj = {
        2: 3, //1
        3: 4, //2
        length: 2, // ->3 -> 4
        push: Array.prototype.push
    }
    obj.push(1);
    // item->1  this->obj
    // obj[obj.length] = 1 -> obj[2] = 1
    // obj.length++
    obj.push(2);
    // item->2  this->obj
    // obj[3] = 2
    // obj.length++
    console.log(obj);

数据转换的技巧

题目使打印OK

if (a == 1 && a == 2 && a == 3) {
	console.log('OK');
}
方案1:重写覆盖valueOf/toString方法

对象转换为数字,需要调取valueOf/toString

    var a = {
        i: 0,
        toString() {
            // this->a
            return ++this.i;
        }
    };

或者

    let a = [1, 2, 3];
    a.toString = a.shift;
方法2:数据劫持
  • Object.defineProperty/ Proxy
  • 获取a,如果a不是变量,就是window的一个属性
    let i = 0;
    Object.defineProperty(window, 'a', {
        get() {
            return ++i;
        }
    });
    if (a == 1 && a == 2 && a == 3) {
        console.log('OK');
    }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值