javascript基础——基于原型的语言

本文讨论 javascript 一个老生常谈的问题------原型。我们从基础的原型出发梳理一下js的类型系统、继承等常见问题。

javascript 是一门 "怪异" 的语言,它是具有:

  1. C系语言的语法
  2. 类java:自动内存管理GC ,如果这个也算得话 :)
  3. 类Schema:函数为一等公民(支持函数式编程的先决条件)
  4. 类Self:基于原型继承

等特点的多范式编程语言。 在过去的时间里,java是web开发的龙头老大,大部分js程序员首先是java程序员,他们在js中实践 面向对象编程思想时,不免会对js的原型继承感到相当程度的怪异, js的面相对象好像和熟悉的面相对象不太一样。

当前端成为了一个独立的工种后,更标准的规范也相应推出------ECMAScript。在ES6中,出现了类编程的关键字 class, extends, 然而大部分前端从业人员的第一语言可能还是java或者根本没有编程基础。当我们在js中践行面向对象编程的思想时,能借鉴的依然是使用java式基于类继承的描述:

  1. 将基于类的面相对象实践 迁移到js中,你是否有过橘生淮南淮北的感觉?
  2. 我在工作中只使用class,有必要搞明白原型/原型链吗?
  3. js中有没有类?class 究竟是什么?
  4. 或者:前端生态都在向函数式编程靠拢,我从来不用 class或继承!

原型 是js语言的基石,要搞清楚js的"怪异"行为直面本质是最好的方法。欢迎大家阅读本文,质疑js -> 理解js -> 成为js

术语

  • value :表示任意一个合法的javascript值
  • value: <type>: 表示任意一个合法的javascript值,且其类型是 type
    • value: Object: 表示任意一个合法的javascript对象(狭义)
    • value: Array: 表示任意一个合法的javascript数组
    • value: Nil:表示一个 undefiendnull
    • value: Ref: 表示任意一个合法的javascript值,引用类型
  • value:<!type>: 表示任意一个合法的javascript值,但除了 type 类型的值,与value: <type> 互补
  • [[<property>]]: 表示一个javascript的 内部属性,仅供javascript引擎读取,无法通过对象访问语法直接读取

原型

在javascript中,任意 value: !Nil 都拥有一个 内部属性 [[Prototype]], 引用任意一个 value。 若有 Avalue: !Nil , Bvalue, 且 A.[[Prototype]] === B。则称 BA 的原型。

访问原型

内部属性仅供js引擎使用,语言层面无法直接访问,对于 [[Prototype]] ,通常我们有如下三种访问方式:

  • Object.getPrototypeOf(<value: !Nil>): 推荐, es5
  • Reflect.getPrototypeOf(<value: Ref>):推荐,es6新增,注意类型
  • <value: !Nil>.__proto__:不推荐,非标准属性,定义在Object.prototype
javascript 复制代码
const chicken = Object.create(null) // 创建一个空对象,并将null作为其原型
chicken.type = 'chicken'

const ikun = Object.create(chicken) // 创建一个空对象,并将 chicken 作为其原型
ikun.name = 'ikun'

console.log('ikun', ikun)
console.log('原型', Object.getPrototypeOf(ikun))
console.log('原型', Reflect.getPrototypeOf(ikun))

修改原型

由于 value: !Nil 与其原型 是一种通过引用([[Prototype]])关联的松散关系,因此可以任意修改 value: !Nil[[Prototype]]

  • Object.setPrototypeOf(<value: !Nil>, <value>)
  • Reflect.setPrototypeOf(<value: Object>, <value>)
  • <value: !Nil>.__proto__ = value: 不推荐
javascript 复制代码
// [[Prototype]] -> null
const obj = Object.create(null) 

// [[Prototype]] -> {age: 18}
Object.setPrototypeOf(obj, {age: 18}) 

// [[Prototype]] -> {friend: '胡彦斌'}
Reflect.setPrototypeOf(obj, {friend: '胡彦斌'}) 

原型链

原型 本身是一个 value, 且 任意一个 value: !Nil 都具有 内部属性 [[Prototype]], 所以原型可能还有原型,通过[[Prototype]] 引用就形成了类似 单向链表 的数据结构,称之为 原型链 ,直到 value: Nil 为止,因为 value: Nil不在具有内部属性[[Prototype]]了,如果你没有显示手动设置的话,通常为null

遮蔽性

当访问 value: !Nil 的属性时,会先查询其自身是否具有该属性,若存在即返回,否则逐级往 [[Prototype]] 上查询,直到原型链的尽头 null 依然不存在 则返回 undefined

继承

由于 原型链 具有遮蔽性的特点,因此,javascript可以基于原型链实现 "继承"。之所以打上双引号,是因为此 "继承" 非彼 继承 (基于类的继承,代表语言:Java)。

继承是一种 代码复用的解决方案,避免重复书写相同的代码。在生物学中,子代拷贝一份双亲的基因副本实现了继承,在基于类继承的语言中也是如此, 对一个类多次实力化,每一次都会重复执行拷贝。但javascript基于原型的继承并非如此,value: !Nil 并没有拷贝一份原型的副本,仅仅是通过[[Prototype]]引用了另一个value

基于原型继承的类型系统

函数

value: Function是javascript中特殊的对象,与其他value 不同的是, 函数具有 prototype 属性,注意与 [[Prototype]] 区分。

  • [[Prototype]] 是内部属性,他引用了上文中我们提到的 原型 (回顾一下,原型是任意合法的javascript值value)。
  • prototype 是函数的自有属性,主要为 new 调用时消费的,实际上使用new调用的是prototype.constructor, 这个属性通常引用其自身。
  • 注意:箭头函数 没有 prototype 属性,这也是对尖头函数使用new调用会报not a constructor异常的原因,
javascript 复制代码
function add () {}
const multi = () => {}

console.dir(add)
console.dir(multi)

构造函数调用与实例对象

当对函数使用 关键字 new 调用时,就是通常所说的构造函数调用方式。此时, 表达式会返回一个新的对象,并将该对象的[[Prototype]]指向函数的 prototype

demo

javascript 复制代码
function Person (name, age) {
	this.name = name
	this.age = age
}

const wells = new Person('harrison wells', 18)
console.dir(wells)
console.dir(Person)
console.log(Object.getPrototypeOf(wells) === Person.prototype) // -> true

new 模拟

严格的说,javascript中并没有构造函数,只是借用基于类继承语言的概念,任何函数,只要以new 调用,我们称之为 构造函数调用 。==即:技术上讲,构造函数和普通函数并无区别。== 为了搞清楚这 new 调用方式与普通函数调用方式的区别,我们来简单的模拟一下new 的行为。

  • 若原函数 return 了对象,则返回该对象, 否则:
  • 生成一个新的对象
  • 将函数的 prototype 属性引用的值设置为新对象的 原型
  • this 指向这个新对象
  • 返回这个新对象
javascript 复制代码
function Person (name, age) {
	this.name = name
	this.age = age
}

const wells = new Person('wells', 18)

/**
* wells: Person
* {
*     name: 'wells',
*     age: 18
*     [[Prototype]]: Person.prototype
* }
*/

模拟: 由于new是关键字,我们无法自定义一个关键字,只能使用函数来模拟。

javascript 复制代码
const getType = (val) => Object.prototype.toString.call(val)?.slice(8, -1)

const New = (fn, ...rest) => {
	if(typeof fn !== 'function') {
		throw Error(`The first argument needs to accept the function type, but it get ${getType(fn)}`)
	}

    //生成一个新对象,并将fn.prototype作为其原型
	const obj = Object.create(fn.prototype) 
	const res = fn.apply(obj, rest)

    // 原函数如过return了对象就返回这个对象,否则返回新建的对象
	return typeof res === 'object' && res !== null ? res : obj 
}

const harrison = New(Person, 'harrison', 18)

/**
* harrison: Person
* {
*     name: 'harrison',
*     age: 18
*     [[Prototype]]: Person.prototype
* }
*/

我们可以使用构造函数的方式定义数据类型,javascript中内置了一些构造函数, 以下是部分构造函数列表:

构造函数 描述
Number 生成数字对象
String 生成字符串对象
Function 生成函数对象
Object 生成对象(value: Object)
Boolean 生成布尔对象
Array 生成数组对象
js 复制代码
const num = new Number(1)
const str = new String('hello world')
const func = new Function('x', 'y', 'return x + y')
const obj = new Object({name: 1})
const bool = new Boolean(true)
const arr = new Array(1,2,3)

字面量语法糖

引用类型的字面量表示法实际是对应构造函数的语法糖。

js 复制代码
// 以下创建对象的方式是等价的, 对象实例的原型均为 Object.prototype
const obj = {}
const obj1 = Object.create(Object.prototype) 

// 以下创建数组的方式是等价的, 数组实例的原型均为 Array.prototype
const arr = []
const arr1 = new Array()
const arr2 = Array.of()

原始类型的自动装箱/拆箱

在对原始类型做属性访问操作时,会使用对应的构造函数生成一个新的临时对象,用完即销毁,称之为自动装箱。

拆箱的过程正好相反,调用临时对象的 valueOf | toString 方法。

js 复制代码
const str = 'Hello World'
str.toUpperCase() // -> 'HELLO WORLD'

// 等价与

new String(str).toUpperCase().valueOf() // -> 'HELLO WORLD'

// 自动装箱的对象是临时的,用完即销毁
str.name = 'wells'
str.name // -> undefined

// 等价与
new String(str).name = 'wells'
new String(str).name // -> undefined, 注意,这两次装箱是不同的临时对象

可见,如果你没有使用一些骚操作, 默认行为下,js中所有value: !Nil[[Prototype]]都指向对应构造函数的prototype,基于原型之间的相互关联,就形成了 类型系统

类型系统

上图是 javascript 中一些值和构造函数之间的关系,其中重要的有两个构造函数FunctionObject

  • 大部分函数都是构造函数Function的实例,包括构造函数Function本身
  • 所有的构造函数的原型,都指向Object.prototype, 因此,默认行为下,Object.prototype 存在于所有value: !Nil的原型链上。

模拟类

原型继承的一大特点就是原型改变后,会影响所有引用该原型的值,有时候我们希望实例对象创建后,其行为不再因原型对象的改变而改变,是完全独立的,也就是模拟传统的基于类的继承。

寄生组合式继承

javascript 复制代码
// 封装继承函数
function inherit(superType, subType) {
	subType.prototype = Object.create(superType.prototype)
	subType.prototype.constructor = subType
}

// 父类
function Parent(name) {
	this.name = name
	this.type = 'human'
}

// 父类公共方法
Parent.prototype.sayHello = function () {
	return `Hello, I'm ${this.name}`
}

// 子类
function Child(name, age) {
	// 调用父构造函数给实例对象的name属性赋值,同时获取父类的行为
	// name、type 都是每一个实例对象的自有属性,不在通过原型引用了
	Parent.call(this, name) 
	this.age = age
}

// 子类继承父类
inherit(Parent, Child)

// 注意,由于 iherit 中,将子类的原型重写为父类的原型,所以要先inherit,再定义子类原型上的方法或属性,否则会丢失
 
// 子类公共方法
Child.prototype.getAge = function () {
	return this.age
}

const wells = new Child('wells', 18)

Class extends

es6中出现了类语法 Class extends,一句话,他就是 寄生组合式继承 的语法糖。

js 复制代码
class Parent {
	constructor(name) {
    	this.name = name
      	this.type = 'human'
    }
  
	sayHello () {
    	return `Hello, I'm ${this.name}.`
    }
}


class Chid extends Parent {
	constructor(name, age) {
    	super(name)
      	this.age = age
    }
  
  	getAge() {
    	return this.age
    }
}

babeljs 编译后代码,我去掉了兼容的代码,保留了核心部分。 extends -> inherits:

js 复制代码
function _inherits(subClass, superClass) {
	if (typeof superClass !== "function" && superClass !== null) {
		throw new TypeError("Super expression must either be null or a function");
	}

	subClass.prototype = Object.create( superClass.prototype, {
		constructor: { value: subClass, writable: true, configurable: true }
	})

	if (superClass) Object.setPrototypeOf(subClass, superClass);
}
相关推荐
我命由我123451 小时前
React Router 6 - 编程式路由导航、useInRouterContext、useNavigationType
前端·javascript·react.js·前端框架·html·ecmascript·js
橙露3 小时前
JavaScript 异步编程:Promise、async/await 从原理到实战
开发语言·javascript·ecmascript
我命由我123453 小时前
React Router 6 - 嵌套路由、路由传递参数
前端·javascript·react.js·前端框架·html·ecmascript·js
十六年开源服务商4 小时前
2026年WordPress网站地图完整指南
java·前端·javascript
英俊潇洒美少年4 小时前
MessageChannel 如何实现时间切片
javascript·react.js·ecmascript
技术钱6 小时前
react数据大屏四种适配方案
javascript·react.js·ecmascript
李明卫杭州6 小时前
JavaScript 严格模式下 arguments 的区别
前端·javascript
一次旅行6 小时前
今日心理学知识分享(三)
开发语言·javascript·程序人生·ecmascript
牛十二7 小时前
openclaw安装mcporter搜索小红书
开发语言·javascript·ecmascript
小金鱼Y7 小时前
🔥 前端人必看:浏览器安全核心知识点全解析(XSS/CSRF/DDoS)
前端·javascript·安全