高级js学习(6)面向对象阶段。

面向对象

JS中常见的内置类。

  • 一般每一种数据类型都有自己的类。
    如Number,每个数字类型都是他的实例
    String,每个字符串都是他的实例。Boolean,BigInt,Object,Array,Set,Map,Date,Symbol,RegExp,Error,Event,Function
  • 元素对象,如HTMLDivElement等,
    • HTMLDivElement是HTMLElement的实例,而HTMLElement又是Element的实例。Element是Node的实例。Node是EventTarget的实例,EventTarget是Object的实例。
      所以每一个标签都可以使用EventTarget的方法。
  • 元素集合
    THMLCollection => Object
    NodeList => Object

构造函数和普通函数的执行

再复习一下,

  • 首先,浏览器执行js代码之前,会生成一个栈地址。用来存放代码并且执行。而且会产生一个堆地址,用来存放引用类型的值。
  • 初始化的时候堆地址会生成一个GO全局对象,用来存放内置的方法,类,等等。GO地址为0x001。
  • 在执行代码之前,栈地址会生成一个产生一个全局上下文,改上下文有一个VO全局变量对象,用来存放全局上下文的变量。此时里面有个变量叫做window,他的值就是GO的地址0x001,这也就是为什么可通过window.xx去调用浏览器的一些内置方法。
  • 接着执行代码,变量提升,对于var和fcuntion,其变量是存储在GO伤的。var是变量提升不定义,而function是变量提升并且定义!let跟const等的变量就是存放在当前上下文的AO(VO的一种,也是变量对象,对于全局上下问,变量对象就叫VO,其他私有上下文叫做AO)
  • 在这里插入图片描述
 function Fn(x, y) {
            this.x = x
            this.y = y
        }

        const res = Fn(10, 20)

        const f1 = new Fn(10, 20)

        console.log(window.x); // 10
        console.log(f1.x); // 10
  • 执行Fn(10,20)的时候,属于普通函数执行,this指向的是window,thi.x = x,就是给window.x赋值了。而普通函数的执行是:
    * 产生一个私有上下文
    * 然后进行初始化作用域链
    * 初始化this
    * 初始化arguments
    * 形参赋值
    * 变量提升
    * 代码执行

  • 而执行new Fn(10,20)的时候,属于构造函数执行,返回值是类的实例对象。而构造函数执行跟普通函数执行的区别是。
    * 创建当前类的实例 (0x002)
    * 产生一个私有上下文
    * 然后进行初始化作用域链
    * 初始化this (将this指向0x002)
    * 初始化arguments
    * 形参赋值
    * 变量提升
    * 代码执行
    区别1: 第一步就是创建类的实例对象, 0x002
    区别2: 将this指向创建的实例对象,
    区别3: 如果构造函数中,没有写return或者return一个原始类型的值,那么默认返回创建的实例对象,如果显示的返回一个对象,则以返回的对象为主。
    在这里插入图片描述

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

new Fn && new Fn()

都是把构造函数执行,创建实例对象
区别:

  • 第二种可以传参数,第一种不可以
  • 第二种属于无参数new,优先级在19左右,第二种属于带参数new,优先级在20左右。带参数new优先级比较高。
    在这里插入图片描述

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

总结

  • 每次new都会创建一个单独的实例对象
  • 构造函数中,只有this.xxx才是个实例对象创建属性,如果基于自己的作用域单独声明变量,只是一个私有变量,与实例对象没有关系。
  • this.xx设置的都是实例的私有属性,不共享。

检测属性

  • hasOwnProperty 检测私有属性,继承的不行。
  • in 检测是否有这个属性,无论是否私有

原型与原型链

  • 大部分“函数数据类型”的值都有一个prototype(显示)属性,值本身是一个对象(浏览器默认开辟一块堆内存,用来存储实例可调用的共享的属性和方法),在这个原型对象中,有个constructor属性,属性值是当前的函数或者类本身。
  • 函数数据类型
    普通函数(实名/匿名)
    箭头函数
    构造函数/类
    生成器函数gernerator,
  • 不具备prototype的函数
    箭头函数
    基于es6给某个对象的方法赋予函数值的快速操作如const a.= {d(){}},
    类里面的基于es6的方法赋值 calss A { a(){} }
    。。。。
    在这里插入图片描述

在这里插入图片描述
a.a有prototype而a.b没有

每个“对象数据类型”都有一个_proto_(原型链,隐式原型)属性,指向自己所属类的prototyoe,
function Fn(){
	this.x = 100
	this.y = 200
	this.getX = function(){
			return this.x
}
}

Fn.prototype.getX = function(){
		return this.x
}
Fn.prototype.getY = function(){
		return this.y
}

const f1 = new Fn()
const f2 = new Fn()

在这里插入图片描述
每个函数的prototype是一个新的堆内存,对象,用于存储实例共享的属性和方法。如上,Fn是继承于Obejct的,所以Fn.prototype._proto_也是指向Object.prototype。
即,

f1._proto_ = Fn.prototype
Fn.prototype.constructor = Fn
Fn.prototype._proto_  = Object.prototype
Object.prototype.constructor = Object
Object.prototype._proto_ = null

Object上的方法不作为实例共享的方法,只作为工具类方法,调用只能Object.xxx调用,而prototype伤的方法是实例调用的,所以是f1.xxx来调用,
在这里插入图片描述
这两个共享Fn.prototype
在这里插入图片描述
而这些Fn上自己的_proto_,以及Object上的_proto_,甚至Function上自身的_ptoto_,Number,String等,他们作为类(函数),他们所属的类就是Function类,所以他们的_proto_指向Function.prototype。即
Function.proto = Object.proto = String._proto… = Function.prototype
在这里插入图片描述
所有类都是函数,所有函数都是函数,也都是Object的实例。

function Fn(){
	this.x = 100
	this.y = 200
	this.getX = function(){
			return this.x
}
}

Fn.prototype.getX = function(){
		return this.x
}
Fn.prototype.getY = function(){
		return this.y
}

const f1 = new Fn()
const f2 = new Fn()
f1.gexX === f2.getX. // false getX被重写了。此时f1.xx拿到的事自身的私有方法,
f1.getY === f2.getY // true   首先在自身上查找,找不到就去_proto_上找,都是prototype伤的getY


对象.xx的时候,首先先看自身的属性,没有再通过_proto_去类的prototype里面找,再找不到就继续通过_ptoto_.ptoto,即类.prototype._ptoto_继续往上找,
把这种基于_proto_和prototype查找的机制成为原型链机制
很少直接通过_proto_去获取原型,ie不支持,可以通过Object.getPrototypeOf(x)去获取x实例所属类的prototype,等同于x.proto
Fn.prototoype = x.proto = Object.getPrototypeOf(x)

重写new

我们知道,new的时候,函数会被执行。并且返回一个对象。

// 实现Object.create /创建一个新的对象,并且将其_proto_指向prototype,

function create(prototype){
	const obj = {}
	// setPrototypeOf 相当于obj._proto_= protottpe
	Object.setPrototypeOf(obj, prototype)
	//默认指向了
	//obj.__proto__ = prototype
	return obj
}

function Fn(x){
		this.x = x
}
Fn.prototype.say = function(){return this.x}

const f1 = new Fn(3)
f1.say()
 console.log(f1);

//重写new 
function _new(fn, ...args){

	// 边界判断
 	if(typeof fn !== 'function'){throw new Error(`${fn} is not a constructor!`)}
 	if(['Symbol', 'BigInt'].includes(fn.name) || !fn.prototype){
 	  throw new Error(`${fn.name} is not a contructor`)
 	}
 	

	const obj = create(fn.prototype) // 创建一个对象,其_proto_指向fn.prototype
	//执行fn,并且将this指向obj,赋值私有属性和方法
	const result  = fn.call(obj, ...args) //如果返回对象就按照返回的值。
	return Object.prototype.toString.call(result) === '[object Object]' ? result : obj
}

const f2 = _new(Fn, 2)
f2.say()
  console.log(f2);

在这里插入图片描述
这样new就简单实现了。
没有prototype属性的函数不允许new,但是有两个特殊,Symbol, bigInt他们有prototype,但是不能呗new。

function  Fn(){
console.log(1)
}
Fn.prototype.getA = function()  {
console.log(2)
}
new  new Fn().getA() //  1 2 先打印1,打印2

上图,new带参数的优先级是20,new不带参数的优先级是19。成员访问是20.
从左到右,先执行new Fn() 打印1。
然后成员访问获取到getA,再被外城new一下,打印2
正确顺序应该是下面这样。执行两次。

const f = new Fn()
new f.getA()

在这里插入图片描述

es5

基于是否有new来决定是不是创建实例。

es6

基于class创建类

class A {
	y = 2 //也是给实例添加私有属性,与类A无关。
	constructor(){
		//构造函数,给实例添加私有属性
		this.x = 1
	}
	
	//给类的prototype对象添加方法,并且是实例方法中不可枚举的
	getX(){}

	//设置类的私有属性,与实例无瓜
	static a = 1
	static getY(){
		return 1
	}
}

const a = new A()
a.y  // 2
a.x  //1
a.getX() //
a.a // undefined
a.getY() // 报错,undefined不能当作函数调用。
A.a // 1
A.getY() // 1

基于内置类扩展方法

//数组的push的操作类似如下
Array.prototype.push = function push(val){
	this[this.length] = val
	this.length++
	return this.length
}


只有类的实例才能使用该类的原型上的方法,但是可以通过一些其他技巧,使其他类型的实例可以使用。

如 1 方法借用 把需要使用的方法挂在到对象的私有属性上。

const a = {

	2: 3,
	3: 4,
	length: 2,
	push: Array.prototype.push
}

a.push(1)  // a[a.lenght] = a[2] = 1  a.lenght++
a.push(3) // a[3] = 3 a.length++
console.log(a)  // 结果是{2:1, 3:3, length: 4}

2 基于call改变this指向。

Array.prototype.slice = function slice(start = 0, end = 0) {
            let arr = []
            end = end > this.length ? this.length : end
            for (let i = start; i < end; i++) {
                arr.push(this[i])
            }
            return arr
        }
        let utils = (function () {
            function toArray(...args) {
                return args
            }

            function toArray1() {
                return Array.form(arguments)
                //return [...arguments]
            }

            function toArray3() {
                let arr = []
                for (let i = 0; i < arguments.length; i++) {
                    arr.push(arguments[i])
                }
                return arr
            }

            //数组克隆
            function toArray2() {
                //借助slice方法,让其this指向arguments,然后执行slcie方法。
                Array.prototype.slice.call(arguments) //es6之前拷贝数组的方法
            }
            return {
                toArray
            }
        })()

        let ary = utils.toArray(10, 20, 30) // [10, 20, 30]

基于call实现。(特点:借用的方法的对象,不可以是该方法所在类的实例)也称为鸭子类型(长得像鸭子,我门就说他是鸭子,重点是想让他用鸭子的办法。)

数据类型转换和劫持

a === 1 && a === 2 && a ===3 

const a = {
	c:0,
[Symbol.toPrimitive](){
        console.log(123);
		return this.c++
    },

}


对象进行比较的时候,会调用 【Symbol.toPrimitive】=》 toString =>valueof 最后再调用valueof,而toString会将对象转为’[object object]'字符串形式,一旦返回的是原始类型,1就不会继续走下去,所以不会执行valueof,所以必须在[Symbol.toPrimitive]或者toString拦截。

猜你喜欢

转载自blog.csdn.net/lin_fightin/article/details/121128841
今日推荐