《JavaScript语言入门教程》记录整理:面向对象

本系列基于阮一峰老师的《JavaScrip语言入门教程》或《JavaScript教程》记录整理,教程采用知识共享 署名-相同方式共享 3.0协议。这几乎是学习js最好的教程之一(去掉之一都不过分)

最好的教程而阮一峰老师又采用开源方式共享出来,之所以重新记录一遍,一是强迫自己重新认真读一遍学一遍;二是对其中知识点有个自己的记录,加深自己的理解;三是感谢这么好的教程,希望更多人阅读了解

面向对象编程

实例对象与 new 命令

  1. 面向对象编程(Object Oriented ProgrammingOOP)将现实世界中的实物、逻辑操作及各种复杂关系抽象为一个个对象,每一个对象完成一定的功能,用来接受信息、处理数据或执行操作、发布信息等,通过继承还能实现复用和功能扩展。比起由一系列函数或指令组成的传统的过程式编程(procedural programming)更适合大型项目。

  2. 什么是”对象”(object):(1)对象是单个实物的抽象。(2)对象是一个容器,封装了属性(property)和方法(method)。属性是对象的状态,方法是对象的行为(完成某种任务)。

  3. 生成对象时,通常需要一个模板,表示某一类实物的共同特征,然后根据模板生成。在C++、java、c#等语言中都有类(class)的概念。”类”就是对象的模板,对象是”类”的实例(即类的一个具体对象)。JavaScript的对象体系基于构造函数(constructor)和原型链(prototype)构成。

  4. JavaScript 语言中构造函数(constructor)就是对象的模板,描述实例对象的基本结构。”构造函数”就是专门用来生成实例对象的函数。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

  5. 构造函数和普通函数一样,但是有自己的特征和用法。

如下,Vehicle就是构造函数。通常构造函数名字第一个字母大写(与普通函数作区分)。

var Vehicle = function () {
  this.price = 1000;
};

构造函数的特点

  • 函数体内部使用了this关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必须使用new命令。
  1. new命令的作用是执行构造函数,返回一个实例对象。
var Vehicle = function () {
  this.price = 1000;
};

var v = new Vehicle();
v.price // 1000

如果忘记了new命令,就成了构造函数作为普通函数直接调用

为了保证构造函数必须使用new命令,解决办法有两种:

一、可以在构造函数内部使用严格模式。这样不使用new命令直接调用就会报错

var Vehicle = function () {
  'use strict';
  this.price = 1000;
};

var v = Vehicle();  // Uncaught TypeError: Cannot set property 'price' of undefined

严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错

二、在构造函数内部判断是否使用new命令,如果没有,则根据参数返回一个实例对象。

function Vehicle(price) {
  if (!(this instanceof Vehicle)) {
    return new Vehicle(price);
  }

  this.price = price||1000;
};

var v1 = Vehicle();
var v2 = new Vehicle();
  1. 使用new命令时,后面的函数依次执行下面的步骤。
  • 创建一个空对象,作为将要返回的对象实例。
  • 将这个空对象的原型,指向构造函数的prototype属性。
  • 将这个空对象赋值给函数内部的this关键字。
  • 开始执行构造函数内部的代码。

构造函数内部,this指的是一个新生成的空对象。构造函数的目的就是操作一个空对象(即this对象),将其”构造”为需要的样子。

如果构造函数内部有return语句且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

var Vehicle = function () {
  this.price = 1000;
  return 1000;  // 忽略非对象的return语句
};

(new Vehicle()) === 1000

如果return返回的是其他对象而不是this,那么new命令将会返回这个新对象

如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。

function getMessage() {
  return 'this is a message';
}

var msg = new getMessage();
msg // {}
typeof msg // "object"

new命令简化的内部流程,可用下面的代码表示。

function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
  // 将 arguments 对象转为数组
  var args = [].slice.call(arguments);
  // 取出构造函数
  var constructor = args.shift();
  // 创建一个空对象,继承构造函数的 prototype 属性
  var context = Object.create(constructor.prototype);
  // 执行构造函数
  var result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);
  1. 函数内部的new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined
function f() {
  console.log(new.target === f);
}

f() // false
new f() // true

此属性可判断是否使用new命令调用了函数

function f() {
  if (!new.target) {
    throw new Error('请使用 new 命令调用!');
  }
  // ...
}

f() // Uncaught Error: 请使用 new 命令调用!
  1. Object.create() 创建实例对象

通常使用构造函数作为生成实例对象的模板。但是如果没有构造函数只有对象时,可以使用Object.create()方法以一个对象作为模板,生成新的实例对象。

如下,对象person1person2的模板,后者继承了前者的属性和方法。

var person1 = {
  name: '张三',
  age: 38,
  greeting: function() {
    console.log('你好,我是' + this.name + '。');
  }
};

var person2 = Object.create(person1);
person2.name;        // "张三"
person2.name="李四"  // "李四"
person2.greeting()   // 你好,我是李四。

person1.greeting()  // 你好,我是张三。

this关键字

  1. this关键字总是返回一个对象,或指向一个对象。
  2. this就是属性或方法”当前”所在的对象。也就是说,如果改变属性或方法所在的对象,就可以改变this的指向

将对象的属性赋给另一个对象,改变属性所在对象,可以改变this的指向。

如下,通过改变函数f所在的对象,实现this的改变

function f() {
  return '姓名:'+ this.name;
}

var A = {
  name: '张三',
  describe: f
};

var B = {
  name: '李四',
  describe: f
};

f()          // "姓名:"
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"

只要函数被赋给另一个变量,this的指向就会变。

  1. JavaScript中,一切皆对象。运行环境也是对象(顶层函数中,this指向window对象),函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。同时this的指向是动态的

  2. this的本质或this的设计目的:

js的对象在内存的结构是这样的,对象存在堆中,当把对象赋值给一个变量时,实际是将对象在堆中的内存地址赋值给变量。如下,将对象的地址(reference)赋值给变量obj

var obj = { foo:  5 };

读取obj.foo的过程是,先从obj拿到内存地址,然后从该地址读出原始的对象,返回它的foo属性

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。比如上面的属性foo实际保存形式如下,foo属性的值保存在属性描述对象的value属性里面:

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

当属性的值是函数时

var obj = { foo: function () {} };

js将函数单独保存在内存中,将函数的地址赋值给foo属性的value属性。

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

因为函数是单独存在的值,所以可以在不同的环境(上下文)执行

JavaScript允许在函数体内部,引用当前环境的其他变量。

如下,函数体使用的变量x由运行环境提供。

var f = function () {
  console.log(x);
};

由于函数可以在不同的运行环境执行,所以需要一种机制,可以在函数体内部获得当前的运行环境(context)。所以this就被用来设计为,在函数体内部,指代函数当前的运行环境

如下,函数体中this.x就指当前运行环境的x

var f = function () {
  console.log(this.x);
}
  1. this的使用场合
  • 全局环境使用this,指的是顶层对象window
  • 构造函数中的this,指的是实例对象。
  • 对象的方法里面包含thisthis的指向就是方法运行时所在的对象。该方法赋值给另一个对象,会改变this的指向。

关于this的指向并不好把握,比如下面的例子

var obj ={
  foo: function () {
    console.log(this);
  }
};

obj.foo() // obj

如上,通过调用boj对象的foo方法,输出this为当前的obj对象。但是,如果使用下面的形式,都会改变this的指向

// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window

上面代码中,obj.foo是获取出来之后再调用,相当于一个值,这个值在调用的时候,运行环境已经从obj变为了全局环境,this的指向变为了window

可以这样理解,在js引擎内部,obj对象和obj.foo函数储存在两个内存地址,称为地址一和地址二。obj.foo()调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。上面三种情况,都是直接取出地址二进行调用(即取出函数调用),这样的话,运行环境就是全局环境,this指向的是全局环境。上面三种情况等同于下面的代码:

// 情况一
(obj.foo = function () {
  console.log(this);
})()
// 等同于
(function () {
  console.log(this);
})()

// 情况二
(false || function () {
  console.log(this);
})()

// 情况三
(1, function () {
  console.log(this);
})()

this所在的方法不在对象的第一层时,这时this指向当前一层的对象(即当前所在的对象),而不会继承更上面的层。

var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};

a.b.m() // undefined
  1. this使用中注意点:
  • 避免多层this。用于this的指向可变,尽量不要在函数中包含多层this

通过添加指向this的变量,实现多层this的使用

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object

JavaScript严格模式下,如果函数内部的this指向顶层对象,就会报错。

  • 避免使用数组处理方法(mapforeach方法中的参数函数)中的this

mapforeach方法的回调函数中的this指向window对象。解决办法是使用一个中间变量固定this,或者使用this作为mapforeach方法的第二个参数

// 中间变量
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

o.f()
// hello a1
// hello a2

// 第二个参数this
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f()
// hello a1
// hello a2
  • 回调函数中避免使用this(往往会改变指向)。
  1. this的动态切换,既体现了灵活,又使编程变得困难和模糊。js提供了callapplybind方法,来切换/固定this的指向。
  2. Function.prototype.call()函数实例call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在指定的作用域中调用该函数

如下,使用call改变作用域6

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

call方法的第一个参数,应该是一个对象。如果参数为空、nullundefined,则this指向全局对象。

var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

call方法的第一个参数是一个原始值,则原始值会自动转成对应的包装对象,然后传入call方法。

var f = function () {
  return this;
};

f.call(5)   // Number {[[PrimitiveValue]]: 5}

call方法除第一个参数表示调用函数的作用域,其他参数以列表的形式传递,表示函数执行时的参数

func.call(thisValue, arg1, arg2, ...)

call方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false
  1. Function.prototype.apply()apply方法的作用,也是改变this指向,然后再调用该函数。但是它接收的是一个数组作为函数执行时的参数,
func.apply(thisValue, [arg1, arg2, ...])

call一样,第一个参数是this指向的对象。null或undefined表示全局对象。第二个参数是数组,表示传入原函数的参数

apply数组,call列表

(1)找出数组最大元素

js默认没有找出数组最大元素的函数,结合applyMath.max可实现返回数组的最大元素

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15

(2)将数组的空元素变为undefined

结合applyArray构造函数将数组的空元素变成undefined

Array.apply(null, ['a', ,'b'])   // [ 'a', undefined, 'b' ]

forEach等循环方法会跳过空元素,但是不会跳过undefined

(3)转换类似数组的对象

利用数组对象的slice方法,可以将一个类似数组的对象(如arguments对象)转为真正的数组。

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, 空]
Array.prototype.slice.apply({length: 1}) // [空]

(4)绑定回调函数的对象

可以在事件方法等回调函数中,通过apply/call绑定方法调用的对象,修改this指向

var o = new Object();
o.f = function () {
  console.log(this === o);
}

var f = function (){
  o.f.apply(o);
  // 或者 o.f.call(o);
};

// jQuery 的写法
$('#button').on('click', f);

因为apply()/call()方法在绑定函数执行时所在的对象时,还会立即执行函数,因此需要把绑定语句写在一个函数体内。

  1. Function.prototype.bind()bind()方法将函数体内的this绑定到某个对象,然后返回一个新函数。

如下是一个通过赋值导致函数内部this指向改变的示例。

var d = new Date();
d.getTime() // 1596621203097

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

d.getTime赋值给变量print后,方法内部的this由原来指向Date对象实例改为了window对象,print()执行报错。

使用bind()方法绑定函数执行的this指向,可以解决这个问题。

var print = d.getTime.bind(d);
undefined
print()   // 1596621203097

bind()可接受更多参数,将这些参数绑定原函数的参数。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

如上,bind()方法除了绑定this对象,还绑定add()函数的第一个参数x5,然后返回一个新函数newAdd(),这个函数只要再接受一个参数y就能运行了。

bind()第一个参数是nullundefined时,this绑定的是全局对象(浏览器环境为window)

  1. bind()方法特定:
  • 每一次返回一个新函数

这就导致,如果绑定事件时直接使用bind()会绑定为一个匿名函数,导致无法取消事件绑定

element.addEventListener('click', o.m.bind(o));
// 如下取消是无效的
element.removeEventListener('click', o.m.bind(o));

正确写法:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);
  • 结合回调函数使用。将包含this的方法直接当做回调函数,会导致函数执行时改变了this的指向,从而出错。解决办法是使用bind()方法绑定回调函数的this对象。当然,也可使用中间变量固定this

  • 结合call()方法使用。改写一些JS原生方法的使用

如下数组的slice方法

[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

call()方法实质上是调用Function.prototype.call()方法。

// 上面等同于
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

相当于在Array.prototype.slice调用Function.prototype.call,参数为(对象,slice的参数)

类似的写法:

var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);

var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]

pop(a)
a // [1, 2, 3]

更进一步bind的调用也可以改写:在Function.prototype.bind上调用call方法(返回的是一个新方法),方法参数是(this对象,bind方法参数)。即最终结果是在this对象上执行bind方法并传递参数。(有些绕)

function f() {
  console.log(this.v);
}

var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123

对象的继承

  1. 对象的继承可以实现代码的复用
  2. 传统JavaScript的继承是通过”原型对象”(prototype)实现的。即js的原型链继承。ES6引入了class语法,实现基于class的继承
  3. 构造函数的缺点:构造函数中通过给this对象的属性赋值,可以很方便地定义实例对象属性。但是这种方式,同一个构造函数的多个实例之间无法共享属性。
function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.features = {
    species:'猫',
    habits:'肉食夜行动物'
  };
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow   // false
cat1.features === cat2.features   // false

cat1cat2是同一个构造函数的两个实例,因为所有meow方法和features对所有实例具有同样的行为和属性,应该共享而不是每个实例都创建新的方法和属性,没必要又浪费系统资源。

原型对象(prototype)用来在实例间共享属性。

  1. JavaScript继承机制的设计思想:原型对象的所有属性和方法,都能被实例对象共享
  2. JavaScript规定,每个函数都有一个prototype属性,指向一个对象
function f() {}
typeof f.prototype // "object"

普通函数基本不会用prototype属性

构造函数生成实例的时候,构造函数的prototype属性会自动成为实例对象的原型。

function Cat(name, color) {
  this.name = name;
}
Cat.prototype.color = 'white';
Cat.prototype.features = {
    species:'猫',
    habits:'肉食夜行动物'
  };
Cat.prototype.meow = function () {
    console.log('喵喵');
  };
var cat1 = new Cat('大毛');
var cat2 = new Cat('二毛');

原型对象的属性不是实例对象自身的属性。其变动体现在所有实例对象上。

当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。如果实例对象自身就有某个属性或方法,则不会再去原型对象寻找这个属性或方法。

原型对象的作用,是定义所有实例对象共享的属性和方法。这也是被称为原型对象的原因。实例对象可以视作从原型对象衍生出来的子对象。

  1. JavaScript规定,所有对象都有自己的原型对象(prototype)。任何一个对象,都可以充当其他对象的原型;而由于原型对象也是对象,所以它也有自己的原型。这就形成一个”原型链”(prototype chain):对象到原型,再到原型的原型…

  2. 所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。所有对象都继承了Object.prototype的属性。

比如所有对象都有valueOftoString方法,就是从Object.prototype继承的

Object.prototype对象的原型是null。原型链的尽头是null

null没有任何属性和方法,也没有自己的原型

Object.getPrototypeOf(Object.prototype)  // null
  1. 如果对象自身和它的原型,都定义了一个同名属性,则优先读取对象自身的属性,这叫做”覆盖”(overriding)。

  2. prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

function P() {}
P.prototype.constructor === P // true

constructor属性的作用是,可以得知某个实例对象由哪一个构造函数产生。另外,有了constructor属性就可以从一个实例对象新建另一个实例。

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true

借助constructor可以在实例方法中调用自身的构造函数

Constr.prototype.createCopy = function () {
  return new this.constructor();
};
  1. constructor属性表明了原型对象与构造函数之间的关联关系。因此如果修改原型对象,一般需要同时修改constructor属性
function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype = {
  method: function () {}
};

Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true

修改原型对象时,一般要同时修改constructor属性的指向

// 坏的写法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 好的写法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 更好的写法
C.prototype.method1 = function (...) { ... };
  1. constructor属性的name属性返回构造函数的名称。

  2. instanceof表示对象是否为某个构造函数的实例。instanceof做判断时会检查右边构造函数的原型对象(prototype)是否在左边实例对象的原型链上。

v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)

instanceof会检查整个原型链,因此使用instanceof判断时,实例对象的原型链上可能返回多个构造函数的原型对象

var d = new Date();
d instanceof Date // true
d instanceof Object // true

任意对象(除了null)都是Object的实例。

var nullObj=null;
typeof nullObj === 'object' && !(nullObj instanceof Object);  // true

如果一个对象的原型是nullinstanceof的判断就会失真。

利用instanceof可以解决调用构造函数时忘了加new的问题

  1. 构造函数的继承

子类整体继承父类

一、在子类的构造函数中调用父类的构造函数

function Sub(value) {
  Super.call(this); // 继承父类实例的属性
  this.prop = value;
}

// 或者使用另一种写法
function Sub() {
  this.base = Super;
  this.base();
}

二、让子类的原型指向父类的原型,继承父类原型

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';

使用Object.create(Super.prototype)赋值给子类的原型,防止引用赋值,后面的修改影响父类的原型。

上面是比较正确或严谨的写法。比较粗略的写法是直接将一个父类实例赋值给子类的原型

Sub.prototype = new Super();

这种方式在子类中会继承父类实例的方法(通常可能不需要具有父类的实例方法),不推荐

子类中继承父类的单个方法

ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // self code
}
  1. 多重继承:JavaScript不提供多重继承功能,即不允许一个对象同时继承多个对象。

但是可以通过合并两个父类的原型的形式,间接变通的实现多重继承

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}

// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);

// 指定构造函数
S.prototype.constructor = S;

var s = new S();
s.hello // 'hello'
s.world // 'world'

这种子类S同时继承了父类M1M2的模式又称为 Mixin(混入)

  1. JavaScript不是一种模块化编程语言,ES6才开始支持”类”和”模块”。但是可以利用对象实现模块的效果
  2. 模块是实现特定功能的一组属性和方法的封装。所以模块的实现最简单的方式就是把模块写成一个对象,所有模块成员都位于对象里面
  • 把模块写成一个对象
var module1 = new Object({
 _count : 0,
 m1 : function (){
  //...
 },
 m2 : function (){
   //...
 }
});

函数m1m2和属性_count都封装在module1对象中。使用中直接调用这个对象的属性即可。

但是,这种写法暴露了所有的模块成员,内部状态可以被外部改写。比如,在外部直接改写内部_count的值:module1._count = 5;

  • 使用构造函数封装私有变量

如下,通过构造函数封装实例的私有变量

function StringBuilder() {
  var buffer = [];

  this.add = function (str) {
     buffer.push(str);
  };

  this.toString = function () {
    return buffer.join('');
  };
}

如下,私有变量buffer在实例对象中,外部是无法直接访问的。

但是,这种方法将私有变量封装在构造函数中,构造函数会和实例对象一直存在于内存中,无法在使用完成后清除。即构造函数的作用既用来生成实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时占用内存。

  • 构造函数中将私有变量设置为实例属性
function StringBuilder() {
  this._buffer = [];
}

StringBuilder.prototype = {
  constructor: StringBuilder,
  add: function (str) {
    this._buffer.push(str);
  },
  toString: function () {
    return this._buffer.join('');
  }
};

这样私有变量就放在了实例对象中。但是私有变量仍然可以从外部读写

  • 通过立即执行函数封装私有变量

通过”立即执行函数”(Immediately-Invoked Function ExpressionIIFE),通过返回”闭包”的方法和属性,实现将属性和方法封装在一个函数作用域里面,函数内的属性作为私有成员不被暴露。

这就是js模块的基本写法:

var module1 = (function () {
 var _count = 0;
 var m1 = function () {
   //...
 };
 var m2 = function () {
  //...
 };
 return {
  m1 : m1,
  m2 : m2
 };
})();
  • 模块的放大模式
    如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时可以采用”放大模式”(augmentation)。

如下,为模块module1添加新方法,并返回新的module1模块

var module1 = (function (mod){
 mod.m3 = function () {
  //...
 };
 return mod;
})(module1);
  • “宽放大模式”(Loose augmentation)

在立即执行函数的参数中添加空对象,防止加载一个不存在的对象,从而报错或出意外

var module1 = (function (mod) {
 //...
 return mod;
})(window.module1 || {});
  • 全局变量的输入

模块最重要的是”独立性”。因此为了在模块内部调用(使用)全局变量,必须显式地将其他变量输入模块内。

比如,下面module1用到了jQuery库(模块),则可以将其作为参数输入module1。保证模块的独立性,并且表明模块之间的依赖关系

var module1 = (function ($) {
 //...
})(jQuery);

立即执行函数还可以起到类似命名空间的作用

Object对象的方法

  1. Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。

几种特殊的原型:

// 空对象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true

// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true

// 函数的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
  1. Object.setPrototypeOf方法为参数对象设置原型,返回该参数对象。Object.setPrototypeOf(obj,prototypeObj)

new命令可以使用Object.setPrototypeOf方法模拟。

var F = function () {
  this.foo = 'bar';
};
var f = new F();

// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
  1. Object.create方法以一个对象为原型,返回一个实例对象。该实例完全继承原型对象的属性。
// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 实例对象
var B = Object.create(A);

Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true

Object.create方法的实现可以用下面的代码代替

if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

生成新的空对象,如下四种是等价的

var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
var obj4 = {};

Object.create的参数为null可以生成一个不继承任何属性(没有toStringvalueOf方法)的对象

var obj = Object.create(null);

Object.create方法必须指定参数且为对象,否则报错。Object.create创建的对象的原型是引用赋值,即动态继承原型。

Object.create方法还可以接受的第二个参数是属性描述对象,描述的对象属性会添加到实例对象的自身属性上。

var obj = Object.create({}, {
  p1: {
    value: 123,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  p2: {
    value: 'abc',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});

// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';

Object.create方法生成的对象会继承它的原型对象的构造函数。

  1. Object.prototype.isPrototypeOf():实例对象的isPrototypeOf方法判断该对象是否为参数对象原型链上的原型。

Object.prototype位于除了直接继承自null的对象之外的所有对象的原型链上。

Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
  1. 关于__proto__属性。__proto__属性是实例对象的属性,表示实例对象的原型(可读写)。实例对象(或非函数对象)无法通过prototype属性获取原型(只有参数才有prototype属性),而__proto__属性默认应该是私有属性,不应该被读写,并且__proto__属性只有浏览器才需要部署。因此,对原型的读写操作正确做法是使用Object.getPrototypeOf()Object.setPrototypeOf()

Obj可以用__proto__直接设置原型

  1. 关于__proto__prototype属性

如下,为构造函数、实例对象、普通对象中__proto__和prototype的对比

/** 构造函数的__proto__和prototype **/
var P=function(){}

P.prototype
// {constructor: ƒ}

P.__proto__
// ƒ () { [native code] }

P.__proto__===P.prototype
// false

P.__proto__===P.constructor.prototype
// true

P.__proto__===Object.getPrototypeOf(P)
// true

P.__proto__===Function.prototype
// true

P.constructor===Function
// true

/** 实例对象的__proto__和prototype  **/ 
var p=new P()

p.prototype
// undefined
p.__proto__
// {constructor: ƒ}
p.__proto__===Object.getPrototypeOf(p)
// true

p.__proto__===P
// false
p.__proto__===P.prototype
// true

p.constructor===P
// true

/** 实例对象的__proto__和prototype **/
var obj={}

obj.prototype
// undefined

obj.__proto__===Object.getPrototypeOf(obj)
// true

obj.__proto__===Object.prototype
// true

obj.constructor===Object
// true

var nullObj=Object.create(null)

nullObj.__proto__
// undefined
nullObj
// {}无属性

几点总结:

  • js中,对象的原型通过__proto__属性获取,由此组成原型链及原型链的继承。

  • __proto__是对象自带的属性,除了null和原型对象为null的对象之外,所有的对象都有__proto__属性。函数是对象,因此函数也有__proto__属性

  • prototype属性是函数独有的属性,每个函数都有一个prototype属性对象,作用是在实例对象间共享属性和方法。因此prototype只会在构造函数中使用,表示实例对象的原型对象。面向对象中的继承由此实现。

  • __proto__属性指向当前对象的原型对象,即构造函数的prototype属性。

  • constructor属性表示当前对象的构造函数

  • 函数也是对象,因此也拥有__proto__属性,指向当前函数的构造函数的prototype属性。一个函数的constructorFunction__proto__Function.prototype

  1. __proto__属性指向当前对象的原型对象,即构造函数的prototype属性。
var obj = new Object();

obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
  1. 获取一个对象obj的原型对象,有三种办法:
  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

但是 __proto__属性只有浏览器环境才需要部署。obj.constructor.prototype在手动改变原型对象时,可能会失效

如下,将构造函数C的原型对象改为p后。实例对象c.constructor.prototype却没有指向pObject.getPrototypeOf(obj)正确获取原型对象,是获取原型对象推荐使用的方法

var P = function () {};
var p = new P();

var C = function () {};
C.prototype = p;
var c = new C();

c.constructor.prototype === p // false

c.constructor.prototype === P.prototype   // true

Object.getPrototypeOf(c) === p  // true

上面变更原型对象的方法是不正确的。通常修改prototype时,要同时设置constructor属性。

C.prototype = p;
C.prototype.constructor = C;

var c = new C();
c.constructor.prototype === p // true
  1. Object.getOwnPropertyNames()返回对象自身所有属性的键名组成的数组(包括可遍历和不可遍历的所有属性)。

  2. Object.keys返回对象自身所有可遍历的属性名组成的数组

  3. Object.prototype.hasOwnProperty()返回一个属性是否为对象自身的属性

hasOwnProperty方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法

  1. in运算符表示一个对象是否具有某个属性。即检查一个属性是否存在。
'length' in Date // true
'toString' in Date // true

for...in循环可以获取一个对象所有可遍历的属性(自身和继承的属性)

通常使用如下方式,遍历对象自身的属性

for ( var name in object ) {
  if ( object.hasOwnProperty(name) ) {
    /* loop code */
  }
}
  1. 获取一个对象的所有属性(包含自身的和继承的,以及可枚举和不可枚举的所有属性)
function inheritedPropertyNames(obj) {
  var props = {};
  while(obj) {
    Object.getOwnPropertyNames(obj).forEach(function(p) {
      props[p] = true;
    });
    obj = Object.getPrototypeOf(obj);
  }
  return Object.getOwnPropertyNames(props);
}
  1. 对象的拷贝

要拷贝一个对象,需要做到下面两点:

  • 确保拷贝后的对象,与原对象具有同样的原型。
  • 确保拷贝后的对象,与原对象具有同样的实例属性。

如下,为对象拷贝的实现:

function copyObject(orig) {
  var copy = Object.create(Object.getPrototypeOf(orig));
  copyOwnPropertiesFrom(copy, orig);
  return copy;
}

function copyOwnPropertiesFrom(target, source) {
  Object
    .getOwnPropertyNames(source)
    .forEach(function (propKey) {
      var desc = Object.getOwnPropertyDescriptor(source, propKey);
      Object.defineProperty(target, propKey, desc);
    });
  return target;
}

利用ES2017引入的Object.getOwnPropertyDescriptors可以更简便的实现

function copyObject(orig) {
  return Object.create(
    Object.getPrototypeOf(orig),
    Object.getOwnPropertyDescriptors(orig)
  );
}

严格模式(strict mode)

  1. JavaScript提供代码执行的第二种模式:严格模式。严格模式从ES5引入,主要目的为:
  • 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
  • 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
  • 提高编译器效率,增加运行速度。
  • 为未来新版本的 JavaScript 语法做好铺垫。
  1. 严格模式的启用:在代码头部添加一行'use strict';即可。老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。
  2. use strict放在脚本文件的第一行,整个脚本都将以严格模式运行。不在第一行则无效。
  3. use strict放在函数体的第一行,则整个函数以严格模式运行。
  4. 有时需要把不同脚本文件合并到一个文件。这时,如果一个是严格模式另一个不是,则合并后结果将会是不正确的。解决办法是可以把整个脚本文件放在一个立即执行的匿名函数中:
(function () {
  'use strict';
  // some code here
})();
  1. 严格模式下的显式报错

严格模式下js的语法更加严格,许多在正常模式下不会报错的错误代码都会显式的报错

如下几项操作严格模式下都会报错:

  • 只读属性不可写;比如字符串的length属性

  • 不可配置属性无法删除(non-configurable)

  • 只设置了取值器的属性不可写

  • 禁止扩展的对象不可扩展

  • evalarguments 不可用作标识名

正常模式下,如果函数有多个重名的参数,可以用arguments[i]读取。严格模式下属于语法错误。

  • 函数不能有重名的参数

  • 禁止八进制的前缀0表示。八进制使用数字0和字母O表示

  1. 严格模式下的安全限制
  • 全局变量显式声明
  • 禁止this关键字指向全局对象。避免无意中创造全局变量
// 正常模式
function f() {
  console.log(this === window);
}
f() // true

// 严格模式
function f() {
  'use strict';
  console.log(this === undefined);
}
f() // true

严格模式下,函数直接调用时,内部的this表示undefined(未定义),因此可以用callapplybind方法,将任意值绑定在this上面。正常模式下,this指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而nullundefined这两个无法转成对象的值,将被忽略。

  • 函数内部禁止使用 fn.calleefn.caller

  • 禁止使用arguments.calleearguments.caller

arguments.calleearguments.caller是两个历史遗留的变量,从来没有标准化过,现在已经取消

  • 禁止删除变量。严格模式下使用delete命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除。
  1. 静态绑定
  • 禁止使用with语句

  • 创设eval作用域

正常模式下,JavaScript语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:eval作用域。

eval所生成的变量只能用于eval内部。

(function () {
  'use strict';
  var x = 2;
  console.log(eval('var x = 5; x')) // 5
  console.log(x) // 2
})()

eval语句使用严格模式:

// 方式一
function f1(str){
  'use strict';
  return eval(str);
}
f1('undeclared_variable = 1'); // 报错

// 方式二
function f2(str){
  return eval(str);
}
f2('"use strict";undeclared_variable = 1')  // 报错
  • arguments不再追踪参数的变化。严格模式下参数修改,arguments不再联动跟着改变
  1. 面向ECMAScript 6
  • ES5的严格模式只允许在全局作用域或函数作用域声明函数。
  • 保留字。严格模式新增了一些保留字:implementsinterfaceletpackageprivateprotectedpublicstaticyield