JavaScript原型和原型链和constructor简述

168 阅读18分钟

一.什么是原型?

Javascript中有一句话,叫一切皆是对象,当然这句话也不严谨,比如nullundefined就不是对象,除了这俩完全可以说Javascript一切皆是对象。而Javascript对象都有一个叫做原型的公共属性,属性名是 __proto__。这个原型属性是对另一个对象的引用,通过这个原型属性我们就可以访问另一个对象所有的属性和方法。比如:

let numArray = [1, 2, -8, 3, -4, 7];

Array 实例化的对象就有一个原型属性指向Array.prototype,变量numArray继承了Array.prototype对象所有的属性和方法。

image.png 这就是为什么可以直接调用像sort() 这种方法:

console.log(numArray.sort()); // -> [-4, -8, 1, 2, 3, 7]

也就是说:

numArray.__proto__ === Array.prototype // true

对于其他对象(函数)也是一样(比如Date(),Function(), String(),Number() 等);

当一个构造函数被创建后,实例对象会继承构造函数的原型属性,这是构造函数的一个非常重要的特性。在Javascript中使用new关键字来对构造函数进行实例化。看下面的例子:

const Car = function(color, model, dateManufactured) {
  this.color = color;
  this.model = model;
  this.dateManufactured = dateManufactured;
};
Car.prototype.getColor = function() {
    return this.color;
};
Car.prototype.getModel = function() {
    return this.model;
};
Car.prototype.carDate = function() {
    return `This ${this.model} was manufactured in the year ${this.dateManufactured}`
}
let firstCar = new Car('red', 'Ferrari', '1985');
console.log(firstCar);
console.log(firstCar.carDate());

image.png 上面的例子中,方法getColor,carDate,getModel都是对象(函数)Car的方法,而Car的实例对象firstCar可以继承Car原型上的一切方法和属性。

结论:

  1. js分为函数对象和普通对象,每个对象都有__proto__属性,指向它的构造函数的原型对象,但是只有函数对象才有prototype属性。
  2. Object、Function都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String

那么__proto__和prototype到底是什么?

属性__proto__是一个对象,它有两个属性,constructor和__proto__,constructor属性,用于记录实例是由哪个构造函数创建;

二.什么是原型链?

在Javascript中如果访问一个对象本身不存在的属性或是方法,就首先在它的原型对象上去寻找,如果原型对象上也不存在,就继续在原型对象的原型对象上去寻找,直到找到为止。那么原型对象有尽头么?所有对象的原型尽头是Object.prototype,那么Object.prototype这个对象的 __proto__指向啥呢?答案是null。我们日常开发中用到的绝大多数对象的 __proto__基本不会直接指向Object.prototype,基本都是指向另一个对象。比如所有的函数的 __proto__都会指向Function.prototype,所有数组的 __proto__ 都会指向Array.prototype。

let protoRabbit = {
  color: 'grey',
  speak(line) {
        console.log(`The ${this.type} rabbit says ${line}`);
  }
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "assassin";
killerRabbit.speak("SKREEEE!");

上面代码中变量protoRabbit设置为所有兔子对象的公有属性对象集,killerRabbit这只兔子通过Object.create方法继承了protoRabbit的所有属性和方法,然后给killerRabbit赋值了一个type属性,再看下面的代码:

let mainObject = {
    bar: 2
};
// create an object linked to `anotherObject`
let myObject = Object.create( mainObject );
for (let k in myObject) {
  console.log("found: " + k);
}
// found: bar
("bar" in myObject); 

如上变量myObject本身并没有bar属性,但这里会循着原型链一层一层往上找,直到找到或者原型链结束为止。如果到原型链尽头还是没找到该属性,那么访问该属性的时候就会返回undefined了。

使用for...in关键字对对象进行迭代的过程,和上面访问某个属性循着原型链查找类似,会去遍历所有原型链上的属性(不论属性是否可枚举)。

let protoRabbit = {
  color: 'grey',
  speak(line) {
      console.log(`The ${this.type} rabbit says ${line}`);
  }
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "assassin";
killerRabbit.speak("SKREEEE!");

上面的代码中访问speak的使用率很高,但如果我们想创建很多个Rabbit对象,就必须要重复写很多代码。而这正是原型和构造函数的真正用武之地。

let protoRabbit = function(color, word, type) {
  this.color = color;
  this.word = word;
  this.type = type;
};
protoRabbit.prototype.getColor = function() {
    return this.color;
}
protoRabbit.prototype.speak = function() {
    console.log(`The ${this.type} rabbit says ${this.word}`);
}
let killerRabbit = new protoRabbit('grey', 'SKREEEEE!', 'assassin');
killerRabbit.speak();

如上代码,使用构造函数的方式就可以节省很多的代码。

附赠一张原型链的图:

image.png

js之父在设计js原型、原型链的时候遵从以下两个准则

  1. Person.prototype.constructor == Person // 准则1:原型对象(即Person.prototype)的constructor指向构造函数本身
  2. person.proto == Person.prototype // 准则2:实例(即person)的__proto__和原型对象指向同一个地方

创建对象的六种方法

1.字面量对象

这是比较常用的一种方式:

let obj = {};

2.object方式创建

先通过object构造器new一个对象,再往里丰富成员信息。

var obj = new Object();
obj.name = "dongjc";
obj.age = 32;
obj.Introduce = function () {
        alert("My name is " + this.name + ".I'm " + this.age);
    };
obj.Introduce();

3.Object.create方式创建

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

var a = {a: 1}; 
// a ---> Object.prototype ---> null
var b = Object.create(a);
b.__proto__ === a; // true

优点:   支持当前所有非微软版本或者 IE9 以上版本的浏览器。允许一次性地直接设置 __proto__ 属性,以便浏览器能更好地优化对象。同时允许通过 Object.create(null) 来创建一个没有原型的对象。

缺点:  不支持 IE8 以下的版本;这个慢对象初始化在使用第二个参数的时候有可能成为一个性能黑洞,因为每个对象的描述符属性都有自己的描述对象。当以对象的格式处理成百上千的对象描述的时候,可能会造成严重的性能问题。

4.构造函数创建

构造函数创建的方式更多用来在Javascript中实现继承,多态,封装等特性。

function Animal(name) {
    this.name = name;
}
let cat = new Animal('Tom');

5.使用工厂模式创建对象 这种方式是使用一个函数来创建对象,减少重复代码,解决了前面三种方式的代码冗余的问题,但是方法不能共享的问题还是存在。

// 使用工厂模式创建对象
	// 定义一个工厂方法
	function createObject(name) {
		var o = new Object();
		o.name = name;
		o.sayName = function() {
			alert(this.name);
		};
		return o;
	}

	var o1 = createObject('zhang');
	var o2 = createObject('li');

	//缺点:调用的还是不同的方法
	//优点:解决了前面的代码重复的问题
	alert(o1.sayName === o2.sayName); //false

6.class创建

class关键字是ES6新引入的一个特性,它其实是基于原型和原型链实现的一个语法糖。

class Animal {
  constructor(name) {
    this.name = name;
  }
}
let cat = new Animal('Tom');

设置原型的四种方法

1.通过构造函数的prototype 往原型对象上添加属性

   function Animal(name) {
     this.name = name;
    }
    Animal.prototype.eat = function(){}
   let cat = new Animal('Tom');
   cat.eat()

2.通过对象的 __proto__ 属性动态设置对象的原型

var a = { n: 1 }; 
var b = { m : 2 };
a.__proto__ = b; 
a.__proto__ === b; // true

优点:  支持所有现代非微软版本以及 IE11 以上版本的浏览器。将 __proto__ 设置为非对象的值会静默失败,并不会抛出错误。

缺点:  应该完全将其抛弃因为这个行为完全不具备性能可言;干扰浏览器对原型的优化;不支持 IE10 及以下的浏览器版本。

3.通过 Object.create 初始化设置对象的原型

var a = {a: 1}; 
var b = Object.create(a)
b.__proto__ === a; // true

4.Object.setPrototypeOf 动态设置对象的原型

语法:

Object.setPrototypeOf(obj, prototype)

参数:

参数名含义
obj要设置其原型的对象。
prototype该对象的新原型(一个对象 或 null).
var a = { n: 1 };
var b = { m : 2 };
Object.setPrototypeOf(a, b);
a.__proto__ === b; // true

优点:  支持所有现代浏览器和微软IE9+浏览器。允许动态操作对象的原型,甚至能强制给通过 Object.create(null) 创建出来的没有原型的对象添加一个原型。

缺点:  这个方式表现并不好,应该被弃用;动态设置原型会干扰浏览器对原型的优化;不支持 IE8 及以下的浏览器版本。

var a = { n: 1 };
var b = { m : 2 };
a.__proto__ = b;
a.__proto__ === b; // true

三.根据原型来谈谈对象的继承

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class(ES6 引入了class 语法),而是通过“原型对象”(prototype)实现。那么在JS中常见的继承方式有几种呢?

常见的六种继承方式

方式一、原型链继承

这种方式关键在于:子类型的原型为父类型的实例对象。

       //父类型
       function Person(name, age) {
           this.name = name,
           this.age = age,
           this.play = [1, 2, 3]
           this.setName = function () { }
       }
       Person.prototype.setAge = function () { }
       //子类型
       function Student(price) {
           this.price = price
           this.setScore = function () { }
       }
       Student.prototype = new Person() // 子类型的原型为父类型的实例对象
       var s1 = new Student(15000)
       var s2 = new Student(14000)
       console.log(s1,s2)

image.png

但这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性

子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。

       s1.play.push(4)
       console.log(s1.play, s2.play)
       console.log(s1.__proto__ === s2.__proto__)//true
       console.log(s1.__proto__.__proto__ === s2.__proto__.__proto__)//true

image.png

s1中play属性发生变化,与此同时,s2中play属性也会跟着变化。

另外注意一点的是,我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后

       function Person(name, age) {
           this.name = name,
           this.age = age
       }
       Person.prototype.setAge = function () {
           console.log("111")
       }
       function Student(price) {
           this.price = price
           this.setScore = function () { }
       }
       // Student.prototype.sayHello = function () { }//在这里写子类的原型方法和属性是无效的,
      //因为会改变原型的指向,所以应该放到重新指定之后
       Student.prototype = new Person()
       Student.prototype.sayHello = function () { }
       var s1 = new Student(15000)
       console.log(s1)

特点

  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现

缺点

  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
  • 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中

方式二: 借用构造函数继承

这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

<script type="text/javascript">
  function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    /*this.name = name
    this.age = age*/
    this.price = price
  }
  var s1 = new Student('Tom', 20, 15000)

image.png

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。

console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function

特点

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

方式三: 原型链+借用构造函数的组合继承

这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

        function Person(name, age) {
            this.name = name,
            this.age = age,
            this.setAge = function () { }
        }
        Person.prototype.setAge = function () {
            console.log("111")
        }
        function Student(name, age, price) {
            Person.call(this,name,age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = new Person()
        Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
        Student.prototype.sayHello = function () { }
        var s1 = new Student('Tom', 20, 15000)
        var s2 = new Student('Jack', 22, 14000)
        console.log(s1)
        console.log(s1.constructor) //Student
        console.log(p1.constructor) //Person

image.png

这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。

优点

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点

  • 调用了两次父类构造函数,生成了两份实例

方式四: 组合继承优化1

这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点

       function Person(name, age) {
            this.name = name,
                this.age = age,
                this.setAge = function () { }
        }
        Person.prototype.setAge = function () {
            console.log("111")
        }
        function Student(name, age, price) {
            Person.call(this, name, age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = Person.prototype
        Student.prototype.sayHello = function () { }
        var s1 = new Student('Tom', 20, 15000)
        console.log(s1)

image.png

但这种方式没办法辨别是对象是子类还是父类实例化

console.log(s1 instanceof Student, s1 instanceof Person)//true true
console.log(s1.constructor)//Person

优点

  • 不会初始化两次实例方法/属性,避免的组合继承的缺点

缺点

  • 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。

方式五: 组合继承优化2

借助原型可以基于已有的对象来创建对象,var B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

       function Person(name, age) {
            this.name = name,
            this.age = age
        }
        Person.prototype.setAge = function () {
            console.log("111")
        }
        function Student(name, age, price) {
            Person.call(this, name, age)
            this.price = price
            this.setScore = function () {}
        }
        Student.prototype = Object.create(Person.prototype)//核心代码
        Student.prototype.constructor = Student//核心代码
        var s1 = new Student('Tom', 20, 15000)
        console.log(s1 instanceof Student, s1 instanceof Person) // true true
        console.log(s1.constructor) //Student
        console.log(s1)

同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!\

image.png

方式六:ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

       class Person {
            //调用类的构造方法
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            //定义一般的方法
            showName() {
                console.log("调用父类的方法")
                console.log(this.name, this.age);
            }
        }
        let p1 = new  Person('kobe', 39)
        console.log(p1)
        //定义一个子类
        class Student extends Person {
            constructor(name, age, salary) {
                super(name, age)//通过super调用父类的构造方法
                this.salary = salary
            }
            showName() {//在子类自身定义方法
                console.log("调用子类的方法")
                console.log(this.name, this.age, this.salary);
            }
        }
        let s1 = new Student('wade', 38, 1000000000)
        console.log(s1)
        s1.showName()

image.png

优点

  • 语法简单易懂,操作更方便

缺点

  • 并不是所有的浏览器都支持class关键字

typeof 实现原理

typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断number, string, object, boolean, function, undefined, symbol 这七种类型,这种判断能帮助我们搞定一些问题,比如在判断不是 object 类型的数据的时候,typeof能比较清楚的告诉我们具体是哪一类的类型。但是,很遗憾的一点是,typeof 在判断一个 object的数据的时候只能告诉我们这个数据是 object, 而不能细致的具体到是哪一种 object, 比如👉

let s = new String('abc');
typeof s === 'object'// true
s instanceof String // true
复制代码

要想判断一个数据具体是哪一种 object 的时候,我们需要利用 instanceof 这个操作符来判断,这个我们后面会说到。

来谈谈关于 typeof 的原理吧,我们可以先想一个很有意思的问题,js 在底层是怎么存储数据的类型信息呢?或者说,一个 js 的变量,在它的底层实现中,它的类型信息是怎么实现的呢?

其实,js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息👉

  • 000:对象
  • 010:浮点数
  • 100:字符串
  • 110:布尔
  • 1:整数

but, 对于 undefinednull 来说,这两个值的信息存储是有点特殊的。

null:所有机器码均为0

undefined:用 −2^30 整数来表示

所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为0,因此直接被当做了对象来看待。

然而用 instanceof 来判断的话👉

null instanceof null // TypeError: Right-hand side of 'instanceof' is not an object
复制代码

null 直接被判断为不是 object,这也是 JavaScript 的历史遗留bug,可以参考typeof

因此在用 typeof 来判断变量类型的时候,我们需要注意,最好是用 typeof 来判断基本数据类型(包括symbol),避免对 null 的判断。

还有一个不错的判断类型的方法,就是Object.prototype.toString,我们可以利用这个方法来对一个变量的类型来进行比较准确的判断

Object.prototype.toString.call(1) // "[object Number]"

Object.prototype.toString.call('hi') // "[object String]"

Object.prototype.toString.call({a:'hi'}) // "[object Object]"

Object.prototype.toString.call([1,'a']) // "[object Array]"

Object.prototype.toString.call(true) // "[object Boolean]"

Object.prototype.toString.call(() => {}) // "[object Function]"

Object.prototype.toString.call(null) // "[object Null]"

Object.prototype.toString.call(undefined) // "[object Undefined]"

Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
复制代码

instanceof 操作符的实现原理

之前我们提到了 instanceof 来判断对象的具体类型,其实 instanceof 主要的作用就是判断一个实例是否属于某种类型

let person = function () {
}
let nicole = new person()
nicole instanceof person // true
复制代码

当然,instanceof 也可以判断一个实例是否是其父类型或者祖先类型的实例。

let person = function () {
}
let programmer = function () {
}
programmer.prototype = new person()
let nicole = new programmer()
nicole instanceof person // true
nicole instanceof programmer // true
复制代码

这是 instanceof 的用法,但是 instanceof 的原理是什么呢?根据 ECMAScript 语言规范,我梳理了一下大概的思路,然后整理了一段代码如下

function new_instance_of(leftVaule, rightVaule) { 
    let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
    leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
    while (true) {
    	if (leftVaule === null) {
            return false;	
        }
        if (leftVaule === rightProto) {
            return true;	
        } 
        leftVaule = leftVaule.__proto__ 
    }
}
复制代码

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

看几个很有趣的例子

function Foo() {
}

Object instanceof Object // true
Function instanceof Function // true
Function instanceof Object // true
Foo instanceof Foo // false
Foo instanceof Object // true
Foo instanceof Function // true
复制代码

要想全部理解 instanceof 的原理,除了我们刚刚提到的实现原理,我们还需要知道 JavaScript 的原型继承原理。

关于原型继承的原理,我简单用一张图来表示

image.png 我们知道每个 JavaScript 对象均有一个隐式的 __proto__ 原型属性,而显式的原型属性是 prototype,只有 Object.prototype.__proto__ 属性在未修改的情况下为 null 值。根据图上的原理,我们来梳理上面提到的几个有趣的 instanceof 使用的例子。

  • Object instanceof Object

    由图可知,Object 的 prototype 属性是 Object.prototype, 而由于 Object 本身是一个函数,由 Function 所创建,所以 Object.__proto__ 的值是 Function.prototype,而 Function.prototype__proto__ 属性是 Object.prototype,所以我们可以判断出,Object instanceof Object 的结果是 true 。用代码简单的表示一下

    leftValue = Object.__proto__ = Function.prototype;
    rightValue = Object.prototype;
    // 第一次判断
    leftValue != rightValue
    leftValue = Function.prototype.__proto__ = Object.prototype
    // 第二次判断
    leftValue === rightValue
    // 返回 true
    复制代码
    

    Function instanceof FunctionFunction instanceof Object 的运行过程与 Object instanceof Object 类似,故不再详说。

  • Foo instanceof Foo

    Foo 函数的 prototype 属性是 Foo.prototype,而 Foo 的 __proto__ 属性是 Function.prototype,由图可知,Foo 的原型链上并没有 Foo.prototype ,因此 Foo instanceof Foo 也就返回 false 。

    我们用代码简单的表示一下

    leftValue = Foo, rightValue = Foo
    leftValue = Foo.__proto = Function.prototype
    rightValue = Foo.prototype
    // 第一次判断
    leftValue != rightValue
    leftValue = Function.prototype.__proto__ = Object.prototype
    // 第二次判断
    leftValue != rightValue
    leftValue = Object.prototype = null
    // 第三次判断
    leftValue === null
    // 返回 false
    复制代码
    
  • Foo instanceof Object

    leftValue = Foo, rightValue = Object
    leftValue = Foo.__proto__ = Function.prototype
    rightValue = Object.prototype
    // 第一次判断
    leftValue != rightValue
    leftValue = Function.prototype.__proto__ = Object.prototype
    // 第二次判断
    leftValue === rightValue
    // 返回 true 
    复制代码
    
  • Foo instanceof Function

    leftValue = Foo, rightValue = Function
    leftValue = Foo.__proto__ = Function.prototype
    rightValue = Function.prototype
    // 第一次判断
    leftValue === rightValue
    // 返回 true 
    复制代码
    

总结

简单来说,我们使用 typeof 来判断基本数据类型是 ok 的,不过需要注意当用 typeof 来判断 null 类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof,但是 instanceof 也可能判断不准确,比如一个数组,他可以被 instanceof 判断为 Object。所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString.call 方法。