HarmonyOS应用实战开发-手写JS中的继承实现以及New、this、instanceof

13 篇文章 3 订阅
1 篇文章 1 订阅

构造函数、原型对象和实例之间的关系

详细了解JS中的原型以及原型链可见 juejin.cn/post/733711…

还是以一个简单的例子复习一下 原型和原型链
在这里插入图片描述
在这里插入图片描述

JS中的原型关系图

在这里插入图片描述

借助的几个工具

1.TS Playground 将TS转义成JS www.typescriptlang.org/play
2.Babel 将高等的JS转成低等的JS babeljs.io/repl
3.JS代码在线格式化 www.bejson.com/jshtml_form…

手写继承

原型链实现

// 父类型
function Super(){
    this.flag = 'super';
}
// 父类型原型上的方法
Super.prototype.getFlag = function(){
    return this.flag;
}

// 子类型
function Sub(){
    this.subFlag = 'sub';
}
// 实现继承。将子类的prototype设置为父类的一个实例
Sub.prototype = new Super();
// 子类型原型上的方法
Sub.prototype.getSubFlag = function(){
    return this.subFlag;
}

const instance = new Sub();

console.log(instance.subFlag); // sub
console.log(instance.flag); // super

使用这种方式实现继承,存在一些问题

一、原型对象上的属性 会被所有实例共享

在通过原型链来实现继承时,原型对象上的属性被会所有实例共享,一旦一个实例修改了原型对象的属性值,会立刻反应到其他实例上

Sub.prototype.flag = 'hahahahah';
这里修改了原型对象Sub.prototype上的flag属性后,所有实例的flag都变成了这个值

二、创建子类型的实例时,不能向父类型的构造函数传递参数

实际上,应该说是 没有办法在不影响所有对象实例的情况下,给父类型的构造函数传递参数,我们传递的参数会成为所有实例的属性

借用构造函数

基本思想:apply() 或 call() 方法,在子类型构造函数的内部调用父类型的构造函数,使得子类型拥有父类型的属性和方法

// 父类型 构造函数,可以传递参数
function Super(properties){
  this.properties = [].concat(properties);
  this.colors = ['red', 'blue', 'green'];
}

function Sub(properties){
  // 继承了 Super,传递参数,互不影响
  Super.apply(this, properties);
}

var instance1 = new Sub(['instance1']);
instance1.colors.push('black');
console.log(instance1.colors); // 'red, blue, green, black'
console.log(instance1.properties[0]); // 'instance1'

var instance2 = new Sub();
console.log(instance2.colors); // 'red, blue, green'
console.log(instance2.properties[0]); // 'undefined'

1.借用构造函数解决了上面提到的两个问题:现在 实例间不会共享属性了,也可以向父类型传递参数了
2.但是这种方法仍然存在一些问题:子类型无法继承父类型原型中的属性。我们只是在子类型的构造函数中调用了父类型的构造函数,没有做其他的,子类型和父类型的原型也就没有任何联系

组合继承

基本思想:使用原型链实现对原型属性和方法的继承,借用构造函数来实现对实例属性的继承。
这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性,从而发挥二者之长

function Super(properties){
  this.properties = [].concat(properties);
  this.colors = ['red', 'blue', 'green'];
}

Super.prototype.log = function() {
  console.log(this.properties[0]);
}

function Sub(properties){
  // 继承了 Super,传递参数,互不影响
  Super.apply(this, properties);
}
// 继承了父类型的原型
Sub.prototype = new Super();
// isPrototypeOf() 和 instance 能正常使用
Sub.prototype.constructor = Sub;

var instance1 = new Sub(['instance1']);
instance1.colors.push('black');
console.log(instance1.colors); // 'red,blue,green,black'
instance1.log(); // 'instance1'

var instance2 = new Sub();
console.log(instance2.colors); // 'red,blue,green'
instance2.log(); // 'undefined'

组合继承看起来很不错,但是也有它的缺点:无论什么情况下,组合继承都会调用两次父类型的构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

寄生组合式继承

基本思想:不必为了指定子类型的原型而调用父类型的构造函数,我们所需要的无非就是父类型原型的一个副本而已
通过借用构造函数来继承属性,通过借用临时构造函数来继承原型

// 用于继承的函数
function inheritPrototype(child, parent) {
  var F = function () {}
  F.prototype = parent.prototype;
  child.prototype = new F();
  child.prototype.constructor = child;
}
// 父类型
function Super(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function () {
  console.log(this.name);
};
// 子类型
function Sub(name, age) {
  // 继承基本属性和方法
  SuperType.call(this, name);
  this.age = age;
}

// 继承原型上的属性和方法
inheritPrototype(Sub, Spuer);

Sub.prototype.log = function () {
  console.log(this.age);
};

基于class和extends的继承

class只是一种语法糖,底层实现依然是基于原型的。通过 class 关键字创建的类其实本质上仍然是函数。这个函数可以通过 new 关键字来实例化

constructor中先调用super,之后class中声明的属性按照声明顺序依次初始化,再之后执行constructor内的自定义逻辑

ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象

在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错
在这里插入图片描述
在这里插入图片描述

这段代码里有两条原型链,图中红线
1、构造器原型链

子类的__proto__属性,表示构造函数的继承,总是指向父类

Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

2、实例原型链

子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性

child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

在这里插入图片描述
在这里插入图片描述

extends 继承,主要做了两件事情

1.子类构造函数(Child)的原型(proto)指向了父类构造函数(Parent),子类构造函数Child继承了父类构造函数Parent的里的属性。使用super调用的(ES5则用call或者apply调用传参)

2.把子类实例child的原型对象(Child.prototype) 的原型(proto)指向了父类parent的原型对象(Parent.prototype)

底层实现中,class 声明的类被转化为一个函数,构造函数的逻辑被放在了这个函数的 constructor 方法中,而类的方法则被添加到该函数的原型上

super 关键字用于调用父类的构造函数,确保子类在实例化时能够正确地初始化父类的属性
在这里插入图片描述

"use strict";
// instanceof操作符包含对Symbol的处理
function _instanceof(left, right) {
	if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
		return !! right[Symbol.hasInstance](left);
	} else {
		return left instanceof right;
	}
}
function _callSuper(t, o, e) {
	return o = _getPrototypeOf(o),
	_possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e));
}
// _possibleConstructorReturn 判断Parent。call(this, name)函数返回值 是否为null或者函数或者对象。
function _possibleConstructorReturn(self, call) {
	if (call && (_typeof(call) === "object" || typeof call === "function")) {
		return call;
	} else if (call !== void 0) {
		throw new TypeError("Derived constructors may only return object or undefined");
	}
	return _assertThisInitialized(self);
}
// 如果 self 是void 0 (undefined) 则报错
function _assertThisInitialized(self) {
	if (self === void 0) {
		throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
	}
	return self;
}
function _isNativeReflectConstruct() {
	try {
		var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [],
		function() {}));
	} catch(t) {}
	return (_isNativeReflectConstruct = function _isNativeReflectConstruct() {
		return !! t;
	})();
}
// 获取__proto__
function _getPrototypeOf(o) {
	_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {
		return o.__proto__ || Object.getPrototypeOf(o);
	};
	return _getPrototypeOf(o);
}
// 寄生组合式继承的核心
function _inherits(subClass, superClass) {
	if (typeof superClass !== "function" && superClass !== null) {
		throw new TypeError("Super expression must either be null or a function");
	}
    // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 
    // 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
	subClass.prototype = Object.create(superClass && superClass.prototype, {
		constructor: {
			value: subClass,
			writable: true,
			configurable: true
		}
	});
	Object.defineProperty(subClass, "prototype", {
		writable: false
	});
	if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置__proto__
function _setPrototypeOf(o, p) {
    // Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null
	_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
		o.__proto__ = p;
		return o;
	};
	return _setPrototypeOf(o, p);
}
// 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
function _typeof(o) {
	"@babel/helpers - typeof";
	return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ?
	function(o) {
		return typeof o;
	}: function(o) {
		return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol": typeof o;
	},
	_typeof(o);
}

// 在 _classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,如果不在上面则抛出错误。
// 这一步主要是为了避免直接将 Parent 类当做函数来调用。
// 因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的
function _classCallCheck(instance, Constructor) {
	 // 为什么通过 instanceof 可以判断是否将 Parent 类当函数来调用呢?
	// 因为如果使用 new 操作符实例化 Parent 的时候,那么 instance 就是当前的实例,指向 Parent.prototype,
    // instance instanceof Constructor 必然为true。反之,直接调用 Parent 构造函数,那么 instance 就不会指向 Parent.prototype
	if (!_instanceof(instance, Constructor)) {
		throw new TypeError("Cannot call a class as a function");
	}
}
// 按照它们的属性描述符 把方法和静态属性赋值到构造函数的prototype和构造器函数上
function _defineProperties(target, props) {
	for (var i = 0; i < props.length; i++) {
		var descriptor = props[i];
		descriptor.enumerable = descriptor.enumerable || false;
		descriptor.configurable = true;
		if ("value" in descriptor) descriptor.writable = true;
		// 通过defineProperty来挂载属性
		Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
	}
}
// 把方法和静态属性赋值到构造函数的prototype和构造器函数上
// _createClass 函数接收三个参数,分别是 Constructor (构造函数)、protoProps(需要挂载到原型上的方法)、staticProps(需要挂载到类上的静态方法)。
// 在接收到参数之后,_createClass 会进行判断如果有 staticProps,则挂载到 Constructor 构造函数上;如果有 protoProps ,那么挂载到 Constructor 原型上面。
// 这里的挂载函数 defineProperties 是关键,它对传入的 props 进行了遍历,并设置了其 enumerable(是否可枚举) 和 configurable(是否可配置)、writable(是否可修改)等数据属性。
// 最后使用了 Object.defineProperty 函数来给设置当前对象的属性描述符
function _createClass(Constructor, protoProps, staticProps) {
	// 如果传入了需要挂载的原型方法
	if (protoProps) _defineProperties(Constructor.prototype, protoProps);
	// 如果传入了需要挂载的静态方法
	if (staticProps) _defineProperties(Constructor, staticProps);
	Object.defineProperty(Constructor, "prototype", {
		writable: false
	});
	return Constructor;
}
function _toPropertyKey(t) {
	var i = _toPrimitive(t, "string");
	return "symbol" == _typeof(i) ? i: String(i);
}
function _toPrimitive(t, r) {
	if ("object" != _typeof(t) || !t) return t;
	var e = t[Symbol.toPrimitive];
	if (void 0 !== e) {
		var i = e.call(t, r || "default");
		if ("object" != _typeof(i)) return i;
		throw new TypeError("@@toPrimitive must return a primitive value.");
	}
	return ("string" === r ? String: Number)(t);
}
var Parent =
/*#__PURE__*/
function() {
	function Parent(name) {
		// Parent 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。
		_classCallCheck(this, Parent);
		this.before_name = 'before_name';
		this.name = 'parent';
		this.after_name = 'after_name';
		this.name = name;
	}
	// 创建原型方法
	_createClass(Parent, [{
		key: "sayHello",
		value: function sayHello() {
			console.log("Hello, my name is ".concat(this.name));
		}
	}]);
	return Parent;
} ();
var Child =
/*#__PURE__*/
function(_Parent) {
	_inherits(Child, _Parent);
	function Child(name, age) {
		var _this;
		_classCallCheck(this, Child);
		_this = _callSuper(this, Child, [name]);
		_this.age = 22;
		_this.after2_name = 'after2_name';
		_this.age = age;
		return _this;
	}
	return _createClass(Child);
} (Parent);
var p = new Parent("parant_name");
var c = new Child("child_name", 6);

最外层的 Parent 变量被赋值给了一个立即执行函数,立即执行函数返回的是里面的 Parent 构造函数,实际上最外层的 Parent 就是里面 Parent 的构造函数。

继承实际上是通过通过_inherits修改原型链来实现的。
在这里插入图片描述

_inherits 函数接收两个参数,分别是 subClass (子构造函数)和 superClass (父构造函数),将这个函数做的事情稍微做一下梳理。

1.设置 subClass.prototype 的 [[Prototype]]指向 superClass.prototype 的 [[Prototype]]
2.设置 subClass 的 [[Prototype]] 指向 superClass

总结

ES6 中提供的 class 和 extends 本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承

继承对于JS来说就是父类拥有的方法和属性、静态方法等,子类也要拥有。子类中可以利用原型链查找,也可以在子类调用父类,或者从父类拷贝一份到子类等方案。寄生组合式继承是开发者使用比较多的,主要有三点:

1.子类构造函数的 proto 指向父类构造器,继承父类的静态方法
2.子类构造函数的prototype的__proto__指向父类构造器的prototype,继承父类的方法。
3.子类构造器里调用父类构造器,继承父类的属性
4.在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中

new 实现

1.创建了一个全新的对象。
2.这个对象会被执行[[Prototype]](也就是__proto__)链接。
3.生成的新对象会绑定到函数调用的this。
4.通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
5.如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象

/**
 * 模拟实现 new 操作符
 * @param  {Function} ctor [构造函数]
 * @return {Object|Function|Regex|Date|Error}      [返回结果]
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向构造函数
    newOperator.target = ctor;
    // 1.创建一个全新的对象,
    // 2.并且执行[[Prototype]]链接
    // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
    // Object.create(proto, [propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor构造函数的其余参数
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新对象会绑定到函数调用的`this`。
    // 获取到ctor函数返回结果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null,排除null
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
    return newObj;
}

instanceof 实现

用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上
function Person() {}

var p = new Person();

console.log(p instanceof Object);//true

console.log(p instanceof Person);//true

function myInstanceOf(left, right) {
    // 第一步和第二步:检查左右操作数
    if (typeof left !== 'object' || left === null) {
        return false;
    }
    if (typeof right !== 'function') {
        return false;
    }
    // 第三步:获取原型链的开始
    const proto = right.prototype;
    // 第四步:遍历原型链
    left = left.__proto__;
    while (true) {
        if (left === null) {
            return false;
        }
        if (left === proto) {
            return true;
        }
        left = left.__proto__;
    }
}

this 指向

函数的this在调用时绑定的,完全取决于函数的调用位置(也就是函数的调用方法)
如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。 找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。一一、new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象

function Student(name){
    this.name = name;
	// 相当于返回了
    // return this;

    // return function f(){};
    // return {};
}
var result = new Student('aa');
console.log(result); // {name: 'aa'}
// 如果返回函数f,则result是函数f
// 如果返回是对象{},则result是对象{}
// this绑定到的对象是new生成的对象

二、call 或者 apply( 或者 bind) 调用:严格模式下,绑定到指定的第一个参数。非严格模式下,null和undefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。

const doSth = function(name){
    console.log(this);
    console.log(name);
}
// 非严格模式下,this是第一个参数,会被自动包装成对象。2被包装成Number
doSth.call(2, 'aa'); // Number{2}, 'aa'

const doSth2 = function(name){
    'use strict';
    console.log(this);
    console.log(name);
}
// 严格模式下,this始终是第一个参数,不会被自动包装成对象
doSth2.call(2, 'aa'); // 2, 'aa'

三、对象上的函数调用:绑定到那个对象。但如果把对象方法赋值给一个普通变量,这是需要按照普通函数调用进行查找this
在这里插入图片描述

const studentDoSth = student.doSth; 执行之后,doSth中的this变成了window对象
在这里插入图片描述

四、普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象
在这里插入图片描述

五、ES6 中的箭头函数:不会使用上文的四条标准的绑定规则, 而是根据当前的词法作用域来决定this
箭头函数会继承外层函数调用的 this 绑定( 无论 this 绑定到什么),没有外层函数则绑定到全局对象(浏览器中是window)
在这里插入图片描述

一定要注意,有些调用可能在无意中使用普通函数绑定规则。 如果想“ 更安全” 地忽略 this 绑 定, 你可以使用一个对象, 比如 ø = Object.create(null), 以保护全局对象。

和Java等编译型语言中this的区别

下面这张图准确的解释了在Java和JavaScript中this的传播
在这里插入图片描述

重点知识

1.在Java的实例方法中this会自动传递,不需要显示使用this去调用方法,当然也可以使用this调用,只是没有必要
2.在Java中方法在类上,在JavaScript中方法在原型对象上
3.在JavaScript实例方法中的this,即使调用的bar和baz也在f1这个对象上,如果不是显示使用this调用,this不会自动传递
4.我们必须显示使用this调用方法,this才会往下传递。否则会使用全局的this,比如bar中调用的biff,因为没有显示使用this,它的this是窗口对象window
5.可以使用bind方法将this持久绑定到另外一个对象上

总结

总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。随着鸿蒙的不断发展以及国家的大力支持,未来鸿蒙职位肯定会迎来一个大的爆发,只有积极应对变化,不断学习和提升自己,我们才能在这个变革的时代中立于不败之地。在这里插入图片描述

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值