为您推荐:
精华内容
最热下载
问答
  • 5星
    9MB lpf513 2021-07-07 09:29:46
  • 5星
    11.44MB heart_sun 2021-05-04 14:49:18
  • 5星
    5.62MB heart_sun 2021-05-26 22:19:42
  • 前言之前很多讲原型的文章都会有箭头图,看的我是头晕脑胀的,这次我希望能通过简单的文字,让大家都能彻底彻底理解原型和原型链,都知道虽然JavaScript和Java名字类似,但是两种完全没有联系。其中就包括Java中很...

    前言

    之前很多讲原型的文章都会有箭头图,看的我是头晕脑胀的,这次我希望能通过简单的文字,让大家都能彻底彻底理解原型和原型链,都知道虽然JavaScript和Java名字类似,但是两种完全没有联系。其中就包括Java中很重要的类,在JavaScript中是没有类的概念的,为了复用JavaScript使用了原型和原型链的形式实现了继承。后续ES6推出了class的语法糖,但本质上是对象,可以称之为伪类。伪类是仿照基于类的语言,通过构造器函数产生对象。所以今天的故事我们就从构造函数开始讲起。

    构造函数

    什么是构造函数

    构造函数和普通函数的区别除了人为规定的命名首字母大写之外,最核心的区别在于有没有使用new操作符

    function Person (name,age){

    this.name = name

    this.age = age

    }

    上面的函数仅仅是一个普通的函数只有当这个函数被用于new 对象的时候才能被称为构造函数。

    那么为什么通过new操作符可以生产对象呢?在new的操作中又经历了什么呢?

    1.在构造函数头部隐式的创建一个对象

    2.将构造函数的作用域赋给新对象,将原先指向window的this指向构造出的新对象

    3.返回这个对象(如果函数体内含有return,则分返回函数是否是对象)

    function Person (name,age){

    this.name = name

    this.age = age

    return 1

    }

    new Person('tim',18) // Person {name: "tim", age: 18}

    如果是return非对象则返回构造的对象

    function Person (name,age){

    this.name = name

    this.age = age

    return {tim:18}

    }

    new Person('tim',18) // {tim:18}

    如果是return对象则返回return的对象

    构造函数的作用

    我们开头提到过所谓类就是用来描述事物中的属性和行为的,类的特征是由成员组成的,而属性对应的就是类中的成员变量,而方法对应的就是类中的成员方法。而我们通过构造函数构造出来的对象就能满足这一个要求。

    function Person (name,age){

    this.name = name

    this.age = age

    this.eat = function (food){

    console.log('今天吃了'+food)

    }

    }

    var tim = new Person('tim',18)

    var cope =new Person('cope',18)

    当我们使用同一个构造函数,构造出来的对象,他们就会含有一些相同的属性和公用的方法,而这些所有实例都含有的属性和方法除了放在构造函数体内,我们也可以放在函数的原型上,方便实例继承。

    原型

    prototype

    每一个JavaScript的函数对象上都有一个属性叫做prototype,翻译过来呢就是原型。

    function Man (name,age){

    this.name = name

    this.age = age

    }

    Man.prototype.sex = 'male'

    Man.prototype.needCompanion = true

    var tim = new Man('tim',18)

    var cope =new Man('cope',18)

    console.log(tim.sex,cope.sex) //male male

    上面的例子我们就完成了一次实例的继承,tim和cope都继承了来自原型上的属性sex

    我们可以这样说,tim和cope的原型就是Man.prototype这个对象

    那么这个对象里还有什么内容呢?我们在控制台打印一下这个对象

    {

    "sex": "male",

    "needCompanion": true,

    "constructor": ƒ Man(name,age),

    "__proto__": Object

    }

    我们发现在控制台中除了sex和needCompanion这两个我们自己添加的属性,prototype还有两个属性,而且这两个属性,在控制台中的颜色还是偏淡的,代表了这两个都是隐式属性。

    在了解了什么是prototype的基础上我们尝试去揭开constructor和__proto__的神秘面纱

    constructor

    constructor翻译过来就是构建者的意思,我们可以发现Man.prototype.constructor的指向就是Man这个构造函数。

    由于JS中的对象其实都是指针,所有我们可以发现Man和Man.prototype.constructor其实就是完全相同的

    console.log(Man === Man.prototype.constructor) //true

    __proto__

    这个属性是用来干什么呢?看下面的代码你就明白了

    console.log(tim.__proto__ === Man.prototype) //true

    实例的原型和构造函数可以通过constructor和prototype函数可以相互找到对方,那么实例怎么才能找到实例的原型呢,就是通过__proto__属性找到他的原型,和之前作用域中的提到[[scope]]其实差不多。

    每个对象都有__proto__这个属性,还包括数字类型,布尔类型等

    上面 我们提到的new操作符,在new的过程中,也会将生成的隐式对象的__proto__属性指向构造函数的原型,这样才是完整的实例化过程。 原型的内容差不多就这样了,内容不多理解一下就行

    原型链

    上文提到了除了null,undefiend等类型没有__proto__属性外,其他都有__proto__属性,这就和作用域链一样,是一个链式结构,直到找到头为止。

    tim.__proto__ === Man.prototype

    tim.__proto__.__proto__ === Object.prototype

    tim.__proto__.__proto__.__proto__ === null

    当访问实例上的属性时,会首先查找实例上是否含有该属性,而当这个对象上没有这个属性时就会访问__proto__索引看看上面有没有,如果还没有就继续沿着__proto__向上寻找,这就是原型链。

    写在最后

    这是这个系列的第五篇文章,咕咕咕挺久的了,每一次写文章都会有一些收获,希望接下来我可以做一只勤劳的鸽子。下篇文章打算讲讲this,下面的文章,我都会尽量找一些代表性的题目,这样学习之后都可以加以巩固,那么我们下次再见 了。

    展开全文
    weixin_34514378 2021-03-11 10:49:12
  • 一、什么是原型 原型是Javascript中的继承的基础,JavaScript的继承就是基于原型的继承。 1.1 函数的原型对象 ​ 在JavaScript中,我们创建一个函数A( 就是声明一个函数 ), 那么浏览器就会在内存中创建一个对象B,...

    一、什么是原型

    原型是Javascript中的继承的基础,JavaScript的继承就是基于原型的继承。

    1.1 函数的原型对象

    ​ 在JavaScript中,我们创建一个函数A( 就是声明一个函数 ), 那么浏览器就会在内存中创建一个对象B,而且每个函数都默认会有一个属性
    prototype 指向了这个对象( 即: prototype的属性的值是这个对象
    )。这个对象B就是函数A的原型对象,简称函数的原型。这个原型对象B 默认会有一个属性 constructor 指向了这个函数A (
    意思就是说:constructor属性的值是函数A )。

    ​ 看下面的代码:

    /*
    	声明一个函数,则这个函数默认会有一个属性叫 prototype 。而且浏览器会自动按照一定的规则
    	创建一个对象,这个对象就是这个函数的原型对象,prototype属性指向这个原型对象。这个原型对象
    	有一个属性叫constructor 执行了这个函数
    
    	注意:原型对象默认只有属性:constructor。其他都是从Object继承而来,暂且不用考虑。
    */
    function Person () {
    
    }
    

    下面的图描述了声明一个函数之后发生的事情:

    1.2 使用构造函数创建对象

    ​ 当把一个函数作为构造函数 (理论上任何函数都可以作为构造函数)
    使用new创建对象的时候,那么这个对象就会存在一个默认的不可见的属性,来指向了构造函数的原型对象。 这个不可见的属性我们一般用
    [[prototype]] 来表示,只是这个属性没有办法直接访问到。

    ​ 看下面的代码:

    /*
    	利用构造函数创建一个对象,则这个对象会自动添加一个不可见的属性 [[prototype]], 而且这个属性
    	指向了构造函数的原型对象。
    */
      var p1 = new Person();
    

    观察下面的示意图:

    说明:

    1. 从上面的图示中可以看到,创建p1对象虽然使用的是Person构造函数,但是对象创建出来之后,这个p1对象其实已经与Person构造函数没有任何关系了,p1对象的[[ prototype ]]属性指向的是Person构造函数的原型对象。
    2. 如果使用new Person()创建多个对象,则多个对象都会同时指向Person构造函数的原型对象。
    3. 我们可以手动给这个原型对象添加属性和方法,那么p1,p2,p3…这些对象就会共享这些在原型中添加的属性和方法。
    4. 如果我们访问p1中的一个属性name,如果在p1对象中找到,则直接返回。如果p1对象中没有找到,则直接去p1对象的[[prototype]]属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型—原型链。 后面再讲)。
    5. 如果通过p1对象添加了一个属性name,则p1对象来说就屏蔽了原型中的属性name。 换句话说:在p1中就没有办法访问到原型的属性name了。
    6. 通过p1对象只能读取原型中的属性name的值,而不能修改原型中的属性name的值。 p1.name = “李四”; 并不是修改了原型中的值,而是在p1对象中给添加了一个属性name。

    看下面的代码:

    function Person () {
    
    }
    // 可以使用Person.prototype 直接访问到原型对象
    //给Person函数的原型对象中添加一个属性 name并且值是 "张三"
    Person.prototype.name = "张三";
    Person.prototype.age = 20;
    
    var p1 = new Person();
    /*
       访问p1对象的属性name,虽然在p1对象中我们并没有明确的添加属性name,但是
       p1的 [[prototype]] 属性指向的原型中有name属性,所以这个地方可以访问到属性name
       就值。
       注意:这个时候不能通过p1对象删除name属性,因为只能删除在p1中删除的对象。
    */
    console.log(p1.name);  // 张三
    
    var p2 = new Person();
    console.log(p2.name);  // 张三  都是从原型中找到的,所以一样。
    
    console.log(p1.name === p2.name);  // true
    
    // 由于不能修改原型中的值,则这种方法就直接在p1中添加了一个新的属性name,然后在p1中无法再访问到
    //原型中的属性。
    p1.name = "李四";
    console.log("p1:" + p1.name);
    // 由于p2中没有name属性,则对p2来说仍然是访问的原型中的属性。	
    console.log("p2:" + p2.name);  // 张三  
    

    二、与原型有关的几个属性和方法

    2.1 prototype属性

    ​ prototype 存在于构造函数中 (其实任意函数中都有,只是不是构造函数的时候prototype我们不关注而已) ,他指向了这个构造函数的原型对象。

    ​ 参考前面的示意图。

    2.2 constructor属性

    ​ constructor属性存在于原型对象中,他指向了构造函数

    看下面的代码:

    function Person () {
    }
    console.log(Person.prototype.constructor === Person);	// true
    var p1 = new Person();
      //使用instanceof 操作符可以判断一个对象的类型。  
      //typeof一般用来获取简单类型和函数。
      //而引用类型一般使用instanceof,因为引用类型用typeof 总是返回object。
    console.log(p1 instanceof Person);	// true
    

    我们根据需要,可以Person.prototype 属性指定新的对象,来作为Person的原型对象。

    但是这个时候有个问题,新的对象的constructor属性则不再指向Person构造函数了。

    看下面的代码:

    function Person () {
    
    }
    //直接给Person的原型指定对象字面量。则这个对象的constructor属性不再指向Person函数
    Person.prototype = {
    	name:"志玲",
    	age:20
    };
    var p1 = new Person();
    console.log(p1.name);  // 志玲
    
    console.log(p1 instanceof Person); // true
    console.log(Person.prototype.constructor === Person); //false
    //如果constructor对你很重要,你应该在Person.prototype中添加一行这样的代码:
    /*
    Person.prototype = {
      constructor : Person	//让constructor重新指向Person函数
    }
    */
    

    2.3 proto 属性(注意:左右各是2个下划线)

    ​ 用构造方法创建一个新的对象之后,这个对象中默认会有一个不可访问的属性 [[prototype]] , 这个属性就指向了构造方法的原型对象。


    但是在个别浏览器中,也提供了对这个属性[[prototype]]的访问(chrome浏览器和火狐浏览器。ie浏览器不支持)。访问方式:p1.proto

    ​ 但是开发者尽量不要用这种方式去访问,因为操作不慎会改变这个对象的继承原型链。

    function Person () {
    
    }
    //直接给Person的原型指定对象字面量。则这个对象的constructor属性不再指向Person函数
    Person.prototype = {
    	constructor : Person,
    	name:"志玲",
    	age:20
    };
    var p1 = new Person();
    
    console.log(p1.__proto__ === Person.prototype);	//true
    

    2.4 hasOwnProperty() 方法

    ​ 大家知道,我们用去访问一个对象的属性的时候,这个属性既有可能来自对象本身,也有可能来自这个对象的[[prototype]]属性指向的原型。

    ​ 那么如何判断这个对象的来源呢?

    ​ hasOwnProperty方法,可以判断一个属性是否来自对象本身。

    function Person () {
    
    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";
      //sex属性是直接在p1属性中添加,所以是true
    console.log("sex属性是对象本身的:" + p1.hasOwnProperty("sex"));
      // name属性是在原型中添加的,所以是false
    console.log("name属性是对象本身的:" + p1.hasOwnProperty("name"));
      //  age 属性不存在,所以也是false
    console.log("age属性是存在于对象本身:" + p1.hasOwnProperty("age"));
    

    所以,通过hasOwnProperty这个方法可以判断一个对象是否在对象本身添加的,但是不能判断是否存在于原型中,而且也有可能这个属性不存在。

    也即是说,在原型中的属性和不存在的属性都会返回fasle。

    如何判断一个属性是否存在于原型中呢?

    2.5 in 操作符


    in操作符用来判断一个属性是否存在于这个对象中。但是在查找这个属性时候,现在对象本身中找,如果对象找不到再去原型中找。换句话说,只要对象和原型中有一个地方存在这个属性,就返回true

    function Person () {
    
    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";
    console.log("sex" in p1);		//对象本身添加的,所以true
    console.log("name" in p1);	//原型中存在,所以true
    console.log("age" in p1); 	//对象和原型中都不存在,所以false
    

    回到前面的问题,如果判断一个属性是否存在于原型中:

    如果一个属性存在,但是没有在对象本身中,则一定存在于原型中。

    function Person () {
    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";
    
    //定义一个函数去判断原型所在的位置
    function propertyLocation(obj, prop){
    	if(!(prop in obj)){
    		console.log(prop + "属性不存在");
    	}else if(obj.hasOwnProperty(prop)){
    		console.log(prop + "属性存在于对象中");
    	}else {
    		console.log(prop + "对象存在于原型中");
    	}
    }
    propertyLocation(p1, "age");
    propertyLocation(p1, "name");
    propertyLocation(p1, "sex");
    

    三、组合原型模型和构造函数模型创建对象

    3.1 原型模型创建对象的缺陷


    原型中的所有的属性都是共享的。也就是说,用同一个构造函数创建的对象去访问原型中的属性的时候,大家都是访问的同一个对象,如果一个对象对原型的属性进行了修改,则会反映到所有的对象上面。

    ​ 但是在实际使用中,每个对象的属性一般是不同的。张三的姓名是张三,李四的姓名是李四。

    ​ **但是,这个共享特性对 方法(属性值是函数的属性)又是非常合适的。**所有的对象共享方法是最佳状态。这种特性在c#和Java中是天生存在的。

    3.2 构造函数模型创建对象的缺陷


    在构造函数中添加的属性和方法,每个对象都有自己独有的一份,大家不会共享。这个特性对属性比较合适,但是对方法又不太合适。因为对所有对象来说,他们的方法应该是一份就够了,没有必要每人一份,造成内存的浪费和性能的低下。

    function Person() {
        this.name = "李四";
        this.age = 20;
        this.eat = function() {
            console.log("吃完东西");
        }
    }
    var p1 = new Person();
    var p2 = new Person();
    //每个对象都会有不同的方法
    console.log(p1.eat === p2.eat); //fasle
    

    可以使用下面的方法解决:

    function Person() {
        this.name = "李四";
        this.age = 20;
        this.eat = eat;
    }
    function eat() {
        alert("吃完东西");
    }
    var p1 = new Person();
    var p2 = new Person();
    //因为eat属性都是赋值的同一个函数,所以是true
    console.log(p1.eat === p2.eat); //true
    

    但是上面的这种解决方法具有致命的缺陷:封装性太差。使用面向对象,目的之一就是封装代码,这个时候为了性能又要把代码抽出对象之外,这是反人类的设计。

    3.3 使用组合模式解决上述两种缺陷

    ​ 原型模式适合封装方法,构造函数模式适合封装属性,综合两种模式的优点就有了组合模式。

    //在构造方法内部封装属性
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    //在原型对象内封装方法
    Person.prototype.eat = function (food) {
        console.log(this.name + "爱吃" + food);
    }
    Person.prototype.introduce = function () {
        console.log(`大家好!我的名字叫${this.name},我今年${this.age}。`);
    }
    
    var p1 = new Person("李四", 20);
    var p2 = new Person("张三", 30);
    p1.eat("苹果");
    p2.eat("香蕉");
    p1.introduce()
    p2.introduce()
    

    一、原型链与继承的概念


    继承是所有的面向对象的语言最重要的特征之一。大部分的oop语言的都支持两种继承:接口继承和实现继承。比如基于类的编程语言Java,对这两种继承都支持。从接口继承抽象方法
    (只有方法签名),从类中继承实例方法。

    ​ 但是对JavaScript来说,没有类和接口的概念(ES6之前),所以只支持实现继承,而且继承在 原型链
    的基础上实现的。等了解过原型链的概念之后,你会发现继承其实是发生在对象与对象之间。这是与其他编程语言很大的不同。

    二、原型链的概念

    在JavaScript中,原型链是实现继承的主要方法。其基本思想是利用原型让一个引用类型关联另一个引用类型的属性和方法

    ​ 再回顾下,构造函数、原型(对象)和对象之间的关系。每个构造函数都有一个属性 prototype 指向一个原型对象,每个原型对象也有一个属性
    constructor 指向函数,通过new 构造函数()
    创建出来的对象内部有一个不可见的属性[[prototype]]指向构造函数的原型。当每次访问对象的属性和方法的时候,总是先从p1中找,找不到则再去p1指向的原型中找。

    下面我们开始一步步的构造原型链,来实现继承

    2.1 更换构造函数的原型

    ​ 原型其实就是一个对象,只是默认情况下原型对象是浏览器会自动帮我们创建的,而且自动让构造函数的 prototype 属性指向这个自动创建的原型对象。

    ​ 其实我们完全可以把原型对象更换成一个我们自定义类型的对象。

    看下面的代码:

    function Grandfather() {
        this.id = "孙子~我是你爷爷~"
    }
    
    //定义一个构造函数。
    function Father () {
        // 添加name属性.  默认直接赋值了。当然也可以通过构造函数传递过来
        this.name = "马云";
    }
    //给Father的原型添加giveMoney方法
    Father.prototype.giveMoney = function () {
        console.log("我是Father原型中定义的方法");
    }
    //再定义一个构造函数。
    function Son () {
        //添加age属性
        this.age = 18;
    }
    //关键地方:把Son构造方法的原型替换成Father的对象。
    Son.prototype = new Father();
    //给Son的原型添加getMoney方法
    Son.prototype.getMoney = function () {
        console.log("我是Son的原型中定义的方法");
    }
    //创建Son类型的对象
    var son1 = new Son();
    
    //发现不仅可以访问Son中定义属性和Son原型中定义的方法,也可以访问Father中定义的属性和Father原型中的方法。
    //这样就通过原型完成了类型之间的继承。
    //Son继承了Father中的属性和方法,当然还有Father原型中的属性和方法。
    son1.giveMoney();
    son1.getMoney();
    console.log("Father定义的属性:" + son1.name);
    console.log("Son中定义的属性:" + son1.age);
    

    上面的代码其实就完成了Son继承Father的过程。那么到底是怎么完成的继承呢?

    看下面的示意图:

    说明:

    1. 定义Son构造函数后,我们没有再使用Son的默认原型,而是把他的默认原型更换成了Father类型对象。
    2. 这时,如果这样访问 son1.name, 则先在son1中查找name属性,没有然后去他的原型( Father对象)中找到了,所以是"马云"。
    3. 如果这样访问 son1.giveMoney(), 则现在son1中这个方法,找不到去他的原型中找,仍然找不到,则再去这个原型的原型中去找,然后在Father的原型对象找到了。
    4. 从图中可以看出来,在访问属性和方法的时候,查找的顺序是这样的:对象->原型->原型的原型->…->原型链的顶端。 就像一个链条一样,这样 由原型连成的"链条",就是我们经常所说的原型链。
    5. 从上面的分析可以看出,通过原型链的形式就完成了JavaScript的继承。

    2.2 默认顶端原型

    ​ 其实上面原型链还缺少一环。

    ​ 在 JavaScript 中所有的类型如果没有指明继承某个类型,则默认是继承的 Object 类型。这种 默认继承也是通过原型链的方式完成的。

    下面的图就是一个完整的原型链:

    mark

    说明:

    1. 原型链的顶端一定是Object的原型对象。这也是为什么我们随意创建一个对象,就有很多方法可以调用,其实这些方法都是来自Object的原型对象。
    2. 通过对象访问属性方法的时候,一定是会通过原型链来查找的,直到原型链的顶端。
    3. 一旦有了继承,就会出现多态的情况。假设需要一个Father类型的数据,那么你给一个Father对象,或Son对象都是没有任何问题的。而在实际执行的过程中,一个方法的具体执行结果,就看在原型链中的查找过程了。给一个实际的Father对象则从Fahter的原型链中查找,给一个实际的Son则从Son的原型链中查找。
    4. 因为继承的存在,Son的对象,也可以看出Father类型的对象和Object类型的对象。 子类型对象可以看出一个特殊的父类型对象。

    2.3 测试数据的类型

    ​ 到目前为止,我们有3中方法来测试数据的类型。

    1. **typeof:**一般用来测试简单数据类型和函数的类型。如果用来测试对象,则会一直返回object,没有太大意义。
    console.log(typeof 5); // number
    var v = "abc";
    console.log(typeof v);  // string
    console.log(typeof function () {
    
    });  //funcion
    function Person () {
    
    }
    console.log(typeof new Person()); //object
    
    1. instanceof: 用来测试一个对象是不是属于某个类型。结果为boolean值。
    function Father () {
    }
    function Son () {
    }
    
    Son.prototype = new Father();
    var son = new Son();
    console.log(son instanceof Son);  // true
      // Son通过原型继承了Father
    console.log(son instanceof Father);  // true
      //Father又默认继承了Objcet
    console.log(son instanceof Object); // true
    
    1. isPrototypeOf( 对象 ) : 这是个原型的方法,参数传入一个对象,判断参数对象是不是由这个原型派生出来的。 也就是判断这个原型是不是参数对象原型链中的一环。
    function Father () {
    
    }
    function Son () {
    
    }
    
    Son.prototype = new Father();
    var son = new Son();
    console.log(Son.prototype.isPrototypeOf(son));  // true
    console.log(Father.prototype.isPrototypeOf(son));	// true
    console.log(Object.prototype.isPrototypeOf(son));	// true
    

    2.4 原型链在继承中的缺陷

    ​ 原型链并非完美无缺,也是存在一些问题的。

    父类型的属性共享问题


    在原型链中,父类型的构造函数创建的对象,会成为子类型的原型。那么父类型中定义的实例属性,就会成为子类型的原型属性。对子类型来说,这和我们以前说的在原型中定义方法,构造函数中定义属性是违背的。子类型原型中的属性被所有的子类型的实例所共有,如果有一个实例去更改,则会很快反应的其他的实例上。

    看下面的代码:

    function GirlFriend() {
        this.girls = ["志玲","圆圆"]
    }
    
    function Person() {
    
    }
    // 子类的原型对象中就有一个属性 girls ,是个数组
    Person.prototype = new GirlFriend()
    
    p1 = new Person()
    p2 = new Person()
    console.log(p1.girls)
    console.log(p2.girls)
    
    //给son1的girls属性的数组添加一个元素
    p1.girls.push("亦非")
    console.log(p1.girls)
    
    //这时,发现son2中的girls属性的数组内容也发生了改变
    console.log(p2.girls)  // "志玲", "圆圆", "亦非"
    
    展开全文
    fuchang1 2021-11-12 19:14:17
  • 继承与原型链 关系图如下: JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,...

    继承与原型链

    关系图如下:
    在这里插入图片描述

    JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

    几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

    基于原型链的继承

    继承属性

    JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

    遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 proto

    但它不应该与构造函数 func 的 prototype 属性相混淆。被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性。Object.prototype 属性表示 Object 的原型对象。

    这里演示当尝试访问属性时会发生什么:

    // 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的:
    let f = function () {
       this.a = 1;
       this.b = 2;
    }
    /* 这么写也一样
    function f() {
      this.a = 1;
      this.b = 2;
    }
    */
    let o = new f(); // {a: 1, b: 2}
    
    // 在f函数的原型上定义属性
    f.prototype.b = 3;
    f.prototype.c = 4;
    
    // 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
    // o.[[Prototype]] 有属性 b 和 c
    //  (其实就是 o.__proto__ 或者 o.constructor.prototype)
    // o.[[Prototype]].[[Prototype]] 是 Object.prototype.
    // 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
    // 这就是原型链的末尾,即 null,
    // 根据定义,null 就是没有 [[Prototype]]。
    
    // 综上,整个原型链如下:
    
    // {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
    
    console.log(o.a); // 1
    // a是o的自身属性吗?是的,该属性的值为 1
    
    console.log(o.b); // 2
    // b是o的自身属性吗?是的,该属性的值为 2
    // 原型上也有一个'b'属性,但是它不会被访问到。
    // 这种情况被称为"属性遮蔽 (property shadowing)"
    
    console.log(o.c); // 4
    // c是o的自身属性吗?不是,那看看它的原型上有没有
    // c是o.[[Prototype]]的属性吗?是的,该属性的值为 4
    
    console.log(o.d); // undefined
    // d 是 o 的自身属性吗?不是,那看看它的原型上有没有
    // d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
    // o.[[Prototype]].[[Prototype]] 为 null,停止搜索
    // 找不到 d 属性,返回 undefined
    

    给对象设置属性会创建自有属性。获取和设置属性的唯一限制内置 getter 或 setter 的属性。

    继承方法

    JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。

    当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

    var o = {
      a: 2,
      m: function(){
        return this.a + 1;
      }
    };
    
    console.log(o.m()); // 3
    // 当调用 o.m 时,'this' 指向了 o.
    
    var p = Object.create(o);
    // p是一个继承自 o 的对象
    
    p.a = 4; // 创建 p 的自身属性 'a'
    console.log(p.m()); // 5
    // 调用 p.m 时,'this' 指向了 p
    // 又因为 p 继承了 o 的 m 函数
    // 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a' 
    
    在 JavaScript 中使用原型

    接下去,来仔细分析一下这些应用场景下, JavaScript 在背后做了哪些事情。

    正如之前提到的,在 JavaScript 中,函数(function)是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype 。请注意,以下的代码是独立的(出于严谨,假定页面没有其他的JavaScript代码)。为了最佳的学习体验,我们强烈建议阁下打开浏览器的控制台(在Chrome和火狐浏览器中,按Ctrl+Shift+I即可),进入“console”选项卡,然后把如下的JavaScript代码复制粘贴到窗口中,最后通过按下回车键运行代码。

    function doSomething(){}
    console.log( doSomething.prototype );
    // 和声明函数的方式无关,
    // JavaScript 中的函数永远有一个默认原型属性。
    var doSomething = function(){};
    console.log( doSomething.prototype );
    在控制台显示的JavaScript代码块中,我们可以看到doSomething函数的一个默认属性prototype。而这段代码运行之后,控制台应该显示类似如下的结果:
    
    {
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
    我们可以给doSomething函数的原型对象添加新属性,如下:
    
    function doSomething(){}
    doSomething.prototype.foo = "bar";
    console.log( doSomething.prototype );
    可以看到运行后的结果如下:
    
    {
        foo: "bar",
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
    现在我们可以通过new操作符来创建基于这个原型对象的doSomething实例。使用new操作符,只需在调用doSomething函数语句之前添加new。这样,便可以获得这个函数的一个实例对象。一些属性就可以添加到该原型对象中。
    
    请尝试运行以下代码:
    
    function doSomething(){}
    doSomething.prototype.foo = "bar"; // add a property onto the prototype
    var doSomeInstancing = new doSomething();
    doSomeInstancing.prop = "some value"; // add a property onto the object
    console.log( doSomeInstancing );
    运行的结果类似于以下的语句。
    
    {
        prop: "some value",
        __proto__: {
            foo: "bar",
            constructor: ƒ doSomething(),
            __proto__: {
                constructor: ƒ Object(),
                hasOwnProperty: ƒ hasOwnProperty(),
                isPrototypeOf: ƒ isPrototypeOf(),
                propertyIsEnumerable: ƒ propertyIsEnumerable(),
                toLocaleString: ƒ toLocaleString(),
                toString: ƒ toString(),
                valueOf: ƒ valueOf()
            }
        }
    }
    

    如上所示, doSomeInstancing 中的__proto__是 doSomething.prototype. 但这是做什么的呢?当你访问doSomeInstancing 中的一个属性,浏览器首先会查看doSomeInstancing 中是否存在这个属性。
    如果 doSomeInstancing 不包含属性信息, 那么浏览器会在 doSomeInstancing 的 proto 中进行查找(同 doSomething.prototype). 如属性在 doSomeInstancing 的 proto 中查找到,则使用 doSomeInstancing 中 proto 的属性。
    否则,如果 doSomeInstancing 中 proto 不具有该属性,则检查doSomeInstancing 的 protoproto 是否具有该属性。默认情况下,任何函数的原型属性 proto 都是 window.Object.prototype. 因此, 通过doSomeInstancing 的 protoproto ( 同 doSomething.prototype 的 proto (同 Object.prototype)) 来查找要搜索的属性。
    如果属性不存在 doSomeInstancing 的 protoproto 中, 那么就会在doSomeInstancing 的 protoprotoproto 中查找。然而, 这里存在个问题:doSomeInstancing 的 protoprotoproto 其实不存在。因此,只有这样,在 proto 的整个原型链被查看之后,这里没有更多的 proto , 浏览器断言该属性不存在,并给出属性值为 undefined 的结论。

    让我们在控制台窗口中输入更多的代码,如下:

    function doSomething(){}
    doSomething.prototype.foo = "bar";
    var doSomeInstancing = new doSomething();
    doSomeInstancing.prop = "some value";
    console.log("doSomeInstancing.prop:      " + doSomeInstancing.prop);
    console.log("doSomeInstancing.foo:       " + doSomeInstancing.foo);
    console.log("doSomething.prop:           " + doSomething.prop);
    console.log("doSomething.foo:            " + doSomething.foo);
    console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
    console.log("doSomething.prototype.foo:  " + doSomething.prototype.foo);
    

    结果如下:

    doSomeInstancing.prop:      some value
    doSomeInstancing.foo:       bar
    doSomething.prop:           undefined
    doSomething.foo:            undefined
    doSomething.prototype.prop: undefined
    doSomething.prototype.foo:  bar
    
    使用不同的方法来创建对象和生成原型链
    使用语法结构创建的对象
    var o = {a: 1};
    
    // o 这个对象继承了 Object.prototype 上面的所有属性
    // o 自身没有名为 hasOwnProperty 的属性
    // hasOwnProperty 是 Object.prototype 的属性
    // 因此 o 继承了 Object.prototype 的 hasOwnProperty
    // Object.prototype 的原型为 null
    // 原型链如下:
    // o ---> Object.prototype ---> null
    
    var a = ["yo", "whadup", "?"];
    
    // 数组都继承于 Array.prototype
    // (Array.prototype 中包含 indexOf, forEach 等方法)
    // 原型链如下:
    // a ---> Array.prototype ---> Object.prototype ---> null
    
    function f(){
      return 2;
    }
    
    // 函数都继承于 Function.prototype
    // (Function.prototype 中包含 call, bind等方法)
    // 原型链如下:
    // f ---> Function.prototype ---> Object.prototype ---> null
    
    使用构造器创建的对象

    在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

    function Graph() {
      this.vertices = [];
      this.edges = [];
    }
    
    Graph.prototype = {
      addVertex: function(v){
        this.vertices.push(v);
      }
    };
    
    var g = new Graph();
    // g 是生成的对象,他的自身属性有 'vertices''edges'。
    // 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
    
    使用 Object.create 创建的对象

    ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:

    var a = {a: 1};
    // a ---> Object.prototype ---> null
    
    var b = Object.create(a);
    // b ---> a ---> Object.prototype ---> null
    console.log(b.a); // 1 (继承而来)
    
    var c = Object.create(b);
    // c ---> b ---> a ---> Object.prototype ---> null
    
    var d = Object.create(null);
    // d ---> null
    console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
    
    使用 class 关键字创建的对象

    ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class, constructor,static,extends 和 super。

    "use strict";
    
    class Polygon {
      constructor(height, width) {
        this.height = height;
        this.width = width;
      }
    }
    
    class Square extends Polygon {
      constructor(sideLength) {
        super(sideLength, sideLength);
      }
      get area() {
        return this.height * this.width;
      }
      set sideLength(newLength) {
        this.height = newLength;
        this.width = newLength;
      }
    }
    
    var square = new Square(2);
    
    性能

    在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

    遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。下面给出一个具体的例子来说明它:

    console.log(g.hasOwnProperty('vertices'));
    // true
    
    console.log(g.hasOwnProperty('nope'));
    // false
    
    console.log(g.hasOwnProperty('addVertex'));
    // false
    
    console.log(g.__proto__.hasOwnProperty('addVertex'));
    // true
    

    hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:Object.keys())

    注意:检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。

    错误实践:扩展原生对象的原型
    经常使用的一个错误实践是扩展 Object.prototype 或其他内置原型。

    prototype 和 Object.getPrototypeOf

    function A 有一个叫做 prototype 的特殊属性。该特殊属性可与 JavaScript 的 new 操作符一起使用。对原型对象的引用被复制到新实例的内部 [[Prototype]] 属性。例如,当执行 var a1 = new A(); 时,JavaScript(在内存中创建对象之后,和在运行函数 A() 把 this 指向对象之前)设置 a1.[[Prototype]] = A.prototype;。然后当您访问实例的属性时,JavaScript 首先会检查它们是否直接存在于该对象上,如果不存在,则会 [[Prototype]] 中查找。这意味着你在 prototype 中定义的所有内容都可以由所有实例有效地共享,你甚至可以稍后更改部分 prototype,并在所有现有实例中显示更改(如果有必要的话)。

    像上面的例子中,如果你执行
    var a1 = new A(); var a2 = new A(); 那么 a1.doSomething 事实上会指向 Object.getPrototypeOf(a1).doSomething,它就是你在 A.prototype.doSomething 中定义的内容。也就是说:Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething(补充:实际上,执行 a1.doSomething() 相当于执行 Object.getPrototypeOf(a1).doSomething.call(a1)==A.prototype.doSomething.call(a1))

    简而言之, prototype 是用于类的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致。

    [[Prototype]] 看起来就像递归引用, 如 a1.doSomething、Object.getPrototypeOf(a1).doSomething、Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等等等, 直到它被找到或 Object.getPrototypeOf 返回 null。

    因此,当你执行:

    var o = new Foo();
    

    JavaScript 实际上执行的是:

    var o = new Object();
    o.__proto__ = Foo.prototype;
    Foo.call(o);
    

    (或者类似上面这样的),然后,当你执行:

    o.someProp;
    

    它检查 o 是否具有 someProp 属性。如果没有,它会查找 Object.getPrototypeOf(o).someProp,如果仍旧没有,它会继续查找 Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp。

    结论

    在使用原型继承编写复杂代码之前,理解原型继承模型是至关重要的。此外,请注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题。此外,原生原型不应该被扩展,除非它是为了与新的 JavaScript 特性兼容。

    示例
    B 继承自 A:

    function A(a){
      this.varA = a;
    }
    
    // 以上函数 A 的定义中,既然 A.prototype.varA 总是会被 this.varA 遮蔽,
    // 那么将 varA 加入到原型(prototype)中的目的是什么?
    A.prototype = {
      varA : null,
    /*
    既然它没有任何作用,干嘛不将 varA 从原型(prototype)去掉 ?
    也许作为一种在隐藏类中优化分配空间的考虑 ?
    https://developers.google.com/speed/articles/optimizing-javascript
    如果varA并不是在每个实例中都被初始化,那这样做将是有效果的。
    */
      doSomething : function(){
        // ...
      }
    }
    
    function B(a, b){
      A.call(this, a);
      this.varB = b;
    }
    B.prototype = Object.create(A.prototype, {
      varB : {
        value: null,
        enumerable: true,
        configurable: true,
        writable: true
      },
      doSomething : {
        value: function(){ // override
          A.prototype.doSomething.apply(this, arguments);
          // call super
          // ...
        },
        enumerable: true,
        configurable: true,
        writable: true
      }
    });
    B.prototype.constructor = B;
    
    var b = new B();
    b.doSomething();
    
    最重要的部分是:

    类型被定义在 .prototype 中
    用 Object.create() 来继承

    展开全文
    weixin_51525719 2021-01-22 09:41:11
  • 在JavaScript中,每个函数都有一个prototype属性,这个属性指向函数的原型对象。 function Person(age) { this.age = age } Person.prototype.name = 'kavin' var person1 = new Person() var person2 = new ...

    一、prototype

    在JavaScript中,每个函数都有一个prototype属性,这个属性指向函数的原型对象。

    function Person(age) {
        this.age = age       
    }
    Person.prototype.name = 'kavin'
    var person1 = new Person()
    var person2 = new Person()
    console.log(person1.name) //kavin
    console.log(person2.name)  //kavin

    Person.prototype可以找到Person的显示原型。上述例子中,函数的prototype指向了一个对象,而这个对象正是调用构造函数时创建的实例的原型,也就是person1和person2的原型。

    原型的概念:每一个javascript对象(除null外)创建的时候,就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型中“继承”属性。

    让我们用一张图表示构造函数和实例原型之间的关系:

    二、__proto__

    这是每个对象(除null外)都会有的属性,叫做__proto__,这个属性会指向该对象的隐式原型。

    对象的隐式原型会指向类的显式原型

    function Person() {
    
    }
    var person = new Person();
    console.log(person.__proto__ === Person.prototype); // true

    而关系图:

    三、constructor

    每个原型都有一个constructor属性,指向该关联的构造函数。

    function Person() {
    
    }
    console.log(Person===Person.prototype.constructor)  //true

    所以再更新下关系图:

    function Person() {
    
    }
    
    var person = new Person();
    
    console.log(person.__proto__ == Person.prototype) // true
    console.log(Person.prototype.constructor == Person) // true
    // 顺便学习一个ES5的方法,可以获得对象的原型
    console.log(Object.getPrototypeOf(person) === Person.prototype) // true

    补充说明:

    function Person() {
    
    }
    var person = new Person();
    console.log(person.constructor === Person); // true

    当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

    person.constructor === Person.prototype.constructor

    四、实例与原型

     当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

    function Person() {
    
    }
    
    Person.prototype.name = 'Kevin';
    
    var person = new Person();
    
    person.name = 'Daisy';
    console.log(person.name) // Daisy
    
    delete person.name;
    console.log(person.name) // Kevin

    在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。

    但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.__proto__ ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。

    但是万一还没有找到呢?原型的原型又是什么呢?

    五、原型的原型

     在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

    var obj = new Object();
    obj.name = 'Kevin'
    console.log(obj.name) // Kevin

    其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype ,所以我们再更新下关系图:

    六、原型链

     简单的回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。——摘自《javascript高级程序设计》

    继上述五中所说,那 Object.prototype 的原型呢?

    console.log(Object.prototype.__proto__ === null) // true

    引用阮一峰老师的 《undefined与null的区别》 就是

    null 表示“没有对象”,即该处不应该有值。

    所以 Object.prototype.__proto__ 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。

    所以查找属性的时候查到 Object.prototype 就可以停止查找了。

    最后一张关系图也可以更新为:

    图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

    七、instance of

    instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

    使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false

    [] instanceof Array; // true

    缺点: instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true

    优点:instanceof可以弥补Object.prototype.toString.call()不能判断自定义实例化对象的缺点。

    所以可以用instanceof来区分Object和Array

    [] instanceof Array   //true
    {} instanceof Array   //false

    也可以通过调用constructor来识别

    {}.constructor    //返回object
    [].constructor    //返回Array

    通过Object.prototype.toString.call方法来识别

    Object.prototype.toString.call([])   //["object Array"]
    Object.prototype.toString.call({})   //["object Object"]

    参考链接:

    javascript——原型与原型链 - 雅昕 - 博客园

    前端面试题1:Object.prototype.toString.call() 、instanceof 以及 Array.isArray()三种方法判别数组的优劣和区别_爱折腾的研究僧的博客-CSDN博客

    【转载】js中区分object和array方法 - jaycethanks - 博客园

    这个视频:快速掌握 JS 面试题之『原型和原型链』_哔哩哔哩_bilibili。讲的也很清楚

    展开全文
    weixin_43613849 2021-05-16 14:12:49
  • weixin_43786756 2021-02-13 16:51:33
  • weixin_34405084 2021-03-11 10:50:24
  • cann_cuii 2021-03-12 15:12:38
  • XIAO_A_fighting 2021-05-03 08:26:13
  • qq_43239667 2021-11-21 14:22:33
  • lengye7 2021-06-23 15:57:07
  • weixin_42125732 2021-04-23 09:55:47
  • weixin_53259688 2021-03-19 10:24:50
  • weixin_39769984 2021-06-17 07:08:58
  • PILIpilipala 2021-04-15 11:38:58
  • weixin_43866830 2021-04-03 10:43:35
  • weixin_29325515 2021-03-11 10:49:56
  • qq_51586883 2021-08-23 13:49:26
  • weixin_47783510 2021-03-22 21:42:06
  • dreamgetting 2021-07-23 21:00:48
  • weixin_39863918 2021-03-11 10:50:40
  • qq_53288680 2021-04-12 16:37:52
  • qq_33929420 2021-02-19 11:35:36
  • qq_43085611 2021-03-13 14:18:34
  • qq_43260366 2021-03-26 11:03:43
  • weixin_41309331 2021-07-22 16:05:33
  • gqk01 2021-08-19 22:52:18
  • weixin_30249151 2021-03-11 10:50:42
  • weixin_45844049 2021-07-08 01:01:22
  • weixin_44447255 2021-06-10 11:23:11
  • hnyanzijun1 2021-11-02 15:33:42

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 231,680
精华内容 92,672
关键字:

原型链

友情链接: emailsmtp.rar