JavaScript基础巩固系列——面向对象编程(构造函数、new、对象继承、对象拷贝、严格模式)

全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/13745195.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)

重新学习JavaScript是因为当年转前端有点儿赶鸭子上架的意味,我一直在反思我的知识点总是很零散,不能在脑海中形成一个完整的体系,所以这次想通过再次学习将知识点都串联起来,结合日常开发的项目,达到温故而知新的效果。与此同时,总结一下我认为很重要但又被我遗漏的知识点~

构造函数

  • 不使用new命令调用构造函数会导致函数内部原本指向实例对象的this指向全局对象,致使构造函数中的变量、方法成为全局变量、全局方法。
// 方案一:在构造函数内部使用严格模式(严格模式中,函数内部的this不能指向全局对象)
function Fubar(foo, bar){
  'use strict';
  this._foo = foo;
  this._bar = bar;
}

Fubar()
// TypeError: Cannot set property '_foo' of undefined

// 方案二:构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象
function Fubar(foo, bar) {
  if (!(this instanceof Fubar)) {
    return new Fubar(foo, bar);
  }
  
  // 还可以通过new.target判断,如果当前函数是new命令调用,new.target指向当前函数,否则为undefined。
  // if(!new.target) {
  //   return new Fubar(foo, bar);
  // }

  this._foo = foo;
  this._bar = bar;
}

Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
  • 如果构造函数内部有return语句,且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。
var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000  // false

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

(new Vehicle()).price
// 2000

new命令原理

使用new命令时,它后面的函数依次执行下面的步骤:

  • 创建一个空对象,作为将要返回的对象实例。
  • 将这个空对象的原型,指向构造函数的prototype属性。
  • 将这个空对象赋值给函数内部的this关键字。
  • 开始执行构造函数内部的代码。
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);

可以使用Object.setPrototypeOf()方法模拟:

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

var f = new F();
// 等同于
// 将一个空对象的原型设为构造函数的prototype属性(F.prototype)
var f = Object.setPrototypeOf({}, F.prototype);
// 将构造函数内部的this绑定这个空对象,然后执行构造函数,使得定义在this上面的方法和属性(this.foo),都转移到这个空对象上。
F.call(f);

对象的继承

原型对象

  • 原型对象可以解决构造函数在实现代码复用时多个实例之间无法共享属性从而导致系统资源浪费的问题,原型对象的所有属性和方法都能被实例对象共享。
function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"
  • 对于构造函数来说,生成实例的时候prototype属性会自动成为实例对象的原型,对象到原型,再到原型的原型...形成原型链,原型链的尽头是null
Object.getPrototypeOf(Object.prototype)
// null
  • prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数,可以被所有实例对象继承。
function P() {}
var p = new P();

p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
  • 可以通过constructor属性从一个实例对象新建另一个实例。
function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true
  • 如果不能确定constructor属性是什么函数,可以通过name属性从实例得到构造函数的名称。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
  • 修改原型对象时需要同时修改constructer属性的指向。
// 坏的写法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

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

// 更好的写法
C.prototype.method1 = function (...) { ... };
  • Object.prototype.__proto__可以获取实例对象的原型,只有浏览器需要部署,其他环境可以没有这个属性,所以尽量不用,应使用Object.PrototypeOf()Object.setPrototypeOf()进行读写操作。
var obj = {};
var p = {};

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

instanceof运算符

  • instanceof返回一个布尔值,表示对象是否为某个构造函数的实例。
var d = new Date();
d instanceof Date // true
d instanceof Object // true

// 等同于
Date.prototype.isPrototypeOf(d)  // true
  • 由于任意对象(除了null)都是Object的实例,所以instanceof运算符可以判断一个值是否为非null的对象。
var obj = { foo: 123 };
obj instanceof Object // true

null instanceof Object // false
undefined instanceof Object // false
  • instanceof运算符只能用于对象,不适用原始类型的值。
var s = 'hello';
s instanceof String // false

构造函数的继承

  • 让一个构造函数继承另一个构造函数分两步:
// 第一步:在子类的构造函数中,调用父类的构造函数(在实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性)
function Sub(value) {
  Super.call(this);
  this.prop = value;
}
// 第二步:让子类的原型指向父类的原型(这样子类就可以继承父类原型)
// 不是直接等于Super.prototype,否则后面对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉
Sub.prototype = Object.create(Super.prototype);  
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
  • 子类继承父类的单个方法
ClassB.prototype.print = function() {
  ClassA.prototype.print.call(this);
  // some code
}

多重继承

JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。

// 子类S同时继承了父类M1和M2,这种模式又称为 Mixin(混入)。
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'

对象的拷贝

如果要拷贝一个对象,需要做到下面两件事情。

  • 确保拷贝后的对象,与原对象具有同样的原型。
  • 确保拷贝后的对象,与原对象具有同样的实例属性。
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)
  );
}

严格模式

设计背景

早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从 ES5 进入标准的,主要目的有以下几个:

  • 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
    • 全局变量显式声明。
    • 禁止this关键字指向全局对象。
    • 禁止使用fn.calleefn.caller
    • 禁止使用arguments.calleearguments.caller
    • 禁止删除变量。
  • 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
    • 只读属性不可写,例如对字符串的length属性赋值:'abc'.length = 5
    • 删除不可配置属性(configurable: false)。
    • 只设置了取值器的属性不可写。
    • 禁止扩展的对象不可扩展:Object.preventExtensions(obj);obj.v = 1;
    • evalarguments不可用作标识名。
    • 函数不能有重名的参数:function f(a, a, b) {}
    • 禁止八进制的前缀0表示法:var n = 0100;
  • 提高编译器效率,增加运行速度。
    • 禁止使用with语句。
    • 创设eval作用域。
    • arguments不再追踪参数的变化。
    function f(a) {
      a = 2;
      return [a, arguments[0]];
    }
    f(1); // 正常模式为[2, 2]
    
    function f(a) {
      'use strict';
      a = 2;
      return [a, arguments[0]];
    }
    f(1); // 严格模式为[2, 1]
    
  • 为未来新版本的 JavaScript 语法做好铺垫。
    • 非函数代码块不得声明函数,例如在条件代码块、循环代码块中。
    • 新增保留字implementsinterfaceletpackageprivateprotectedpublicstaticyield

启用方法

在单个脚本的开头或者某个函数的开头使用字符串'use strict'

<script>
  'use strict';
  console.log('这是严格模式');
</script>
function strict() {
  'use strict';
  return '这是严格模式';
}

// 不同模式的脚本合并时应把整个脚本文件放在一个立即执行的匿名函数之中,防止出错。
(function () {
  'use strict';
  // some code here
})();

参考资料

JavaScript 语言入门教程 :https://wangdoc.com/javascript/index.html

原文地址:https://www.cnblogs.com/dreamsqin/p/13745195.html