NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

JavaScript渐入佳境 - 构造函数、new、原型

| Comments

在JavaScript中,对象是一系列的键值对,ECMA-262把对象(object)定义为“属性的无序集合,每个属性存放一个原始值、对象或函数”。

JavaScript是面向对象的语言(而且比C++/Java更加的面向对象),因为所有东西都是对象,包括函数,但是在JavaScript没有类的概念。既然没有类,根据对之前对C++/Java这样的基于类的面向对象语言的理解,应该如何理解JavaScript中构造函数的概念,因为在基于类的面向对象中,构造函数只存在于类当中。

JavaScript构造函数、JavaScript中的constructor属性

在JavaScript中,每个具有原型的对象都会自动获得constructor属性。除了arguments、Enumerator、Error、Global、Math、RegExp、Regular Expression等一些特殊对象之外,其他所有的JavaScript内置对象都具备constructor属性。例如:Array、Boolean、Date、Function、Number、Object、String等。所有主流浏览器均支持该属性。

现在,请暂且不要去思考“具有原型的对象”中原型的意思。

对象的constructor属性返回创建该对象的函数的引用,无论直译或者按照基于类的面向对象语言的理解,我们且把该函数称为“构造函数”。下面是一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 字符串:String()
var str = "张三";
document.writeln(str.constructor); // function String() { [native code] }
document.writeln(str.constructor === String); // true

// 数组:Array()
var arr = [1, 2, 3];
document.writeln(arr.constructor); // function Array() { [native code] }
document.writeln(arr.constructor === Array); // true

// 数字:Number()
var num = 5;
document.writeln(num.constructor); // function Number() { [native code] }
document.writeln(num.constructor === Number); // true

// 自定义对象:Person()
function Person(){
    this.name = "CodePlayer";
}
var p = new Person();
document.writeln(p.constructor); // function Person(){ this.name = "CodePlayer"; }
document.writeln(p.constructor === Person); // true

// 字面量对象:Object()
var o = { "name" : "张三"};
document.writeln(o.constructor); // function Object() { [native code] }
document.writeln(o.constructor === Object); // true

// 自定义函数:Function()
function foo(){
    alert("CodePlayer");
}
document.writeln(foo.constructor); // function Function() { [native code] }
document.writeln(foo.constructor === Function); // true

// 函数的原型:bar()
function bar(){
    alert("CodePlayer");
}
document.writeln(bar.prototype.constructor); // function bar(){ alert("CodePlayer"); }
document.writeln(bar.prototype.constructor === bar); // true

代码中的[native code],表示这是JavaScript的底层内部代码实现,无法显示代码细节。

你会发现连那些看似是基本类型的数字“5”和字符串“张三”(当然Java里面有String类型)都具有constructor属性。特别是定义一个函数foo,foo也有constructor属性,而且指向名字是Function的函数(暂且不管为什么如此)。

通过var str = “张三”;,我创建了一个String对象,通过var num = 5;,我创建了一个Number对象。

在Java或者C++中,通过new关键字调用某个类的构造函数,例如:new SomeConstructor(),来创建对象,知识SomeConstructor和类名一样。

那么在JavaScript,如何自定一个具有类型对象呢?上面的Person例子已经给出了答案。

p对象的constructor是它:function Person(){ this.name = “CodePlayer”; }

那么要创建一个新的p对象,只需要var p = new Person();。表面上的理解和C++或者Java相似。

剩下的疑问是创建这个对象的时候使用了new关键字。它是干什么的?new关键字很容易让你想到C++和Java中通过new来创建新的对象(分配一段内存空间)。

在《JavaScript高级编程》里对new操作符的解释:

new操作符会让构造函数产生如下变化:
1.创建一个新对象
2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3.执行构造函数中的代码(为这个新对象添加属性)
4.返回新对象

MDN上也有介绍new关键字: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new

1
2
3
4
5
6
function Person(){ this.name = "CodePlayer"; console.log(this) }
var p = Person()
$ Window {external: Object, chrome: Object, document: document, WPCOMSharing: Object, RecaptchaTemplates: Object}

var p = new Person()
$ Person {name: "CodePlayer"}

JavaScirpt中的构造函数和普通函数没有什么区别,你一样的可以像普通函数一样调用它,如上例,那么this指向函数执行时的当前对象(关于this指针会在之后的文章中详解)。

但如果通过new关键字来调用函数,该函数就成为了构造函数,this指针就会指向新创建的对象。这就是JavaScript的构造函数。

通过new和构造函数创建对象的问题

new的方式创建对象看上去非常好用,而且和C++或者Java语言很相似,比较容易理解它在创建一个新的对象。但是构造函数方法创建对象存在一个浪费内存的问题。

以下摘抄自阮一峰的文章: http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html

“请看,我们现在为Cat对象添加一个不变的属性”type”(种类),再添加一个方法eat(吃老鼠)。那么,原型对象Cat就变成了下面这样:”

1
2
3
4
5
6
7
8
9
10
11
12
13
function Cat(name,color){
  this.name = name;
  this.color = color;
  this.type = "猫科动物";
  this.eat = function(){alert("吃老鼠");};
}

var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat ("二毛","黑色");
alert(cat1.type); // 猫科动物
cat1.eat(); // 吃老鼠

alert(cat1.eat == cat2.eat); //false

“表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。”

解决这个问题,阮一峰在他的文章中介绍了Prototype模式。我猜测对JavaScript面向对象感兴趣的同志已经无数次看到这样的代码了。

JavaScript原型、Prototype和__proto__

prototype英文翻译过来“原型”

什么是原型?原型是一个从其他对象继承属性的对象。

是不是任何对象都可以是原型?是的。

哪些对象有原型?每个对象都有一个默认的原型。原型本身就是对象,每一个原型本身也存在一个原型。(只有一个例外,默认的对象原型在每条原型链的顶端,其他的原型在原型链的后面)

当我看到上面的回答时,加上我对prototype英文含义的理解,我会认为每个对象都有一个prototype. 但当我写({}).prototype的时候,或者”“.prototype,我却得到了undefined,是不是疯了?

忘记你所理解的关于prototype属性的理解(它其实只是函数对象的一个属性,比如Object.prototype,Function.prototype,Array.prototype,在浏览器控制台查看Object,Function,Array这些值,它们都是函数) - 这个名字很可能是迷惑的根源。

一个对象真正的prototype是内部[[Prototype]]属性. ECMA5介绍了标准的访问方法,Object.getPrototypeOf(object)。这个最新的实现已被Firefox, Safari, Chrome and IE9所支持。另外,除了IE,所有的浏览器都支持非标准的访问方法__proto__,它指向当对象被实例化的时候,用作原型的对象。

那么我想知道prototype属性到底是干什么的?比如Object.prototype,Function.prototype,Array.prototype等。特别是上面的阮一峰介绍的Prototype模式。

Object.prototype属性表示对象Object的原型对象,它是所有对象原型链的根节点。

那其他的呢?好吧,首先,在前面介绍构造函数时已经知道,JavaScript不区分构造函数和其它普通函数,所以每个函数都有prototype属性。反而任何不是方法的,都没有这样的属性。

1
2
3
4
5
6
7
8
9
10
11
//永远不是构造函数的方法,无论如何都是有prototype属性的
Math.max.prototype; //[object Object]

//构造函数也有prototype属性
var A = function(name) {
    this.name = name;
}
A.prototype; //[object Object]

//Math不是一个方法,所以没有prototype属性
Math.prototype; //null

现在可以定义:
一个方法的prototype属性是当这个方法被用作构造函数来创建实例时,赋给该实例的原型(内部Prototype)的对象。非常重要的一点是,要理解方法的prototype属性和实际的prototype(原型)没有任何关系。看下面一段代码和解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//构造器,this作为新对象返回并且它内部的[[prototype]]属性将被设置为构造器默认的prototype属性
var Circle = function(radius) {
    this.radius = radius;
    //next line is implicit, added for illustration only
    //this.__proto__ = Circle.prototype;
}

//扩充 Circle默认的prototype对象的属性因此扩充了每个由它新建实例的prototype对象的属性
Circle.prototype.area = function() {
   return Math.PI*this.radius*this.radius;
}

//创建Circle的两个示例,每个都可以使用相同的真正prototype所拥有属性area
var a = new Circle(3), b = new Circle(4);
a.area().toFixed(2); //28.27
b.area().toFixed(2); //50.27

那么,根据上面的定义,下面的内容非常有趣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Function.prototype
$ function() {}
var func = new Function()
func.__proto__
$ function() {}
var functionA = function (){}
functionA.__proto__
$ function() {}
var myFunction = new Function('users', 'salary', 'return users * salary');
myFunction.__proto__
$ function() {}
Array.prototype
$ []
var arr = new Array()
arr.__proto__
$ []
var array = []
array.__proto__
$ []
Cat.prototype
$ Cat {}

原型链和instanceof

原型链又是什么?

因为每个对象和每个原型(本身)都有一个原型,我们可以想象,一个接一个的对象连接在一起形成一个原型链。原型链的终端总是默认对象(object)的原型,即Object.prototype。你可以在自定义的对象上调用__proto__方法就来自于Object.prototype.__proto__上。

1
2
3
4
5
6
7
8
9
10
function Cat(){}
var cat = new Cat()
cat.__proto__
$ Cat {}
cat.__proto__.__proto__
$ Object {}
Object.prototype
$ Object {}
cat.__proto__.__proto__ === Object.prototype
$ true

原型继承机制是内在且隐式实现的。当对象a要访问属性foo时,Javascript会遍历a的原型链(首先从a自身开始),检查原型链的每一个环节中存在的foo属性。如果找到了foo属性就会将其返回,否则返回undefined值。

1
2
3
4
5
var A = function() {};
A.prototype.constructor == A; //true

var a = new A();
a.constructor == A; //true (a 的constructor属性继承自它的原型)

instanceof与prototype有啥关系?

如果A的prototype属性出现在a的原型链中,则表达式a instanceof A会返回true。这意味着我们可以欺骗instanceof,让它失效。

1
2
3
4
5
6
7
8
9
10
11
var A = function() {}

var a = new A();
a.__proto__ == A.prototype; //true - so instanceof A will return true
a instanceof A; //true;

//mess around with a's prototype
a.__proto__ = Function.prototype;

//a's prototype no longer in same prototype chain as A's prototype property
a instanceof A; //false

使用instanceof操作符需要注意的一件事情是,任何时间判断对象是否是Object的实例,它都会返回true,因为所有对象都继承自Object.prototype。

原始值使用对象包装器判断实例时,比如 ‘foo’ instanceof String返回false。如果使用new操作符创建的字符串,instanceof返回true。所以,请记住instanceof只适用于构造函数创建返回的复杂对象和实例。

总结

看完这篇文章,你需要记住一下几点:

构造函数和普通函数没有区别,只有在结合new关键字时,有特定的作用,可以创建一个对象实例,而该对象实例有一个constructor属性指向创建它的函数,即构造函数。每个对象都有原型,不要误解Prototype属性,它是函数对象的属性(比如Object,Array,Function),真正的原型通过Object.getPrototypeOf(object)和__proto__获取。因为构造函数隐式的执行this.__proto__ = Circle.prototype,所以Prototype模式可以实现对象方法是定义(对象中的属性只是引用,指向的是构造函数Prototype属性上定义的一个方法)。

参考文献:
1.http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html
2.http://www.oschina.net/translate/understanding-javascript-prototypes
3.http://yehudakatz.com/2011/08/12/understanding-prototypes-in-javascript/
4.JavaScript启示录

翻译 React on ES6+

| Comments

原文地址: http://babeljs.io/blog/2015/06/07/react-on-es6-plus/

文章翻译有些不准确,敬请见谅

当我们正在从内到外的重新设计Instagram Web的时候,我们非常享受使用许多ES6+的特性来编写React组件。这让我有机会去说明这些新的语言特性可以改变你写React应用的方式,让它变得更简单也更有趣。

Classes

到目前为止,最明显的变化就是当我们选择使用ES6+中的类定义语法时,如何来编写React组件。相对于使用React.createClass方法来定义一个组件,我们可以使用真正的ES6类来继承React.Component:

1
2
3
4
5
class Photo extends React.Component {
  render() {
    return <img alt={this.props.caption} src={this.props.src} />;
  }
}

立马,你就会注意到一个微妙的不同 - 当定义类时,使用一个更加简洁的语法:

1
2
3
4
5
6
7
8
9
10
// The ES5 way
var Photo = React.createClass({
  handleDoubleTap: function(e) {  },
  render: function() {  },
});
// The ES6+ way
class Photo extends React.Component {
  handleDoubleTap(e) {  }
  render() {  }
}

很明显,我们丢掉了两个括号和一个分号,而且每一个方法声明忽略了一个冒号,一个function关键字和一个逗号。

当使用新的类语法时,所有的生命周期方法(除了一个)都可以像你所期望的那样定义。类的构造函数现在的角色,之前是由componentWillMount来扮演:

1
2
3
4
5
6
7
8
9
10
11
12
// The ES5 way
var EmbedModal = React.createClass({
  componentWillMount: function() {  },
});
// The ES6+ way
class EmbedModal extends React.Component {
  constructor(props) {
    super(props);
    // Operations usually carried out in componentWillMount go here
    // 所有componentWillMount的操作都放在这里
  }
}

属性初始化

在ES6+的类世界,属性类型和默认值都是作为类自己的静态属性。同样,Component的状态初始化可以使用ES7的属性初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// The ES5 way
var Video = React.createClass({
  getDefaultProps: function() {
    return {
      autoPlay: false,
      maxLoops: 10,
    };
  },
  getInitialState: function() {
    return {
      loopsRemaining: this.props.maxLoops,
    };
  },
  propTypes: {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  },
});
// The ES6+ way
class Video extends React.Component {
  static defaultProps = {
    autoPlay: false,
    maxLoops: 10,
  }
  static propTypes = {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  }
  state = {
    loopsRemaining: this.props.maxLoops,
  }
}

ES7属性初始化操作在类的构造函数中,这里指向的是类实例的构造,所以state的初始化可以设置为依赖于this.props。值得注意的是,我们不在需要,针对getter方法,定义prop的默认值,和初始化state对象。

箭头函数

React.createClass方法用来在组件实例方法上执行一些额外的绑定工作来保证,在他们里面,this关键字可以被考虑指向组件的实例。

1
2
3
4
5
6
7
// Autobinding, brought to you by React.createClass
var PostInfo = React.createClass({
  handleOptionsButtonClick: function(e) {
    // Here, 'this' refers to the component instance.
    this.setState({showOptionsModal: true});
  },
});

既然我们不使用React.createClass方法了,当我们用ES6+的类语法定义组件时,似乎我们就需要在我们想要这些行为时,手动的绑定实例方法:

1
2
3
4
5
6
7
8
9
10
11
12
// Manually bind, wherever you need to
class PostInfo extends React.Component {
  constructor(props) {
    super(props);
    // Manually bind this method to the component instance...
    this.handleOptionsButtonClick = this.handleOptionsButtonClick.bind(this);
  }
  handleOptionsButtonClick(e) {
    // ...to ensure that 'this' refers to the component instance here.
    this.setState({showOptionsModal: true});
  }
}

幸运的是,通过结合两个ES6+的特性 - 箭头方法和属性初始化 - 选择性的绑定到组件实例变得轻而易举:

1
2
3
4
5
class PostInfo extends React.Component {
  handleOptionsButtonClick = (e) => {
    this.setState({showOptionsModal: true});
  }
}

The body of ES6 arrow functions share the same lexical this as the code that surrounds them, which gets us the desired result because of the way that ES7 property initializers are scoped. Peek under the hood to see why this works.

动态属性名和模板字符串

对对象字面量的一个增强是,拥有给一个衍生而来的属性名赋值的能力。以前,我们可能需要像下面这样做来设置state对象:

1
2
3
4
5
6
7
var Form = React.createClass({
  onChange: function(inputName, e) {
    var stateToSet = {};
    stateToSet[inputName + 'Value'] = e.target.value;
    this.setState(stateToSet);
  },
});

现在,我们可以构建那些属性名由JavaScript表达式在运行时决定的对象。这里,我们用模板字符串来决定将哪个属性设置到state上:

1
2
3
4
5
6
7
class Form extends React.Component {
  onChange(inputName, e) {
    this.setState({
      [`${inputName}Value`]: e.target.value,
    });
  }
}

析构和JSX扩展属性(spread attributes)

Often when composing components, we might want to pass down most of a parent component’s props to a child component, but not all of them. In combining ES6+ destructuring with JSX spread attributes, this becomes possible without ceremony: 常常当我们组合组件时,我们也许想要将父组件的大部分属性传递到子组件中,但是并不是全部。结合ES6+的destructuring和JSX spread attributes,这变成可能且不太复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AutoloadingPostsGrid extends React.Component {
  render() {
    var {
      className,
      ...others,  // contains all properties of this.props except for className
    } = this.props;
    return (
      <div className={className}>
        <PostsGrid {...others} />
        <button onClick={this.handleLoadMoreClick}>Load more</button>
      </div>
    );
  }
}

我们可以将JSX扩展属性和正常的属性相结合,利用简单地优先级规则来实现复写和默认值指定。下面这个元素会获得className “override”,即便className属性已经在this.props中定义:

1
2
3
<div {...this.props} className="override">
  
</div>

这个元素会正常的拥有className “base”,除非在this.props中存在一个className属性复写了它:

1
2
3
<div className="base" {...this.props}>
  
</div>