镇楼图

Pixiv:torino



四、Function类型

Rest语法

一些函数如Math.max可以支持任意数量的参数,JS中对于这样的参数可以简单使用...来实现,使用剩余参数,它支持收集剩余的参数为一数组,可枚举数组实现相应功能

备注:剩余参数只能在最后,若在中间无法判别其他参数的情况

function sum(intial , ...args){
	for(let arg of args)intial += arg;
	return intial;
}
console.log(sum(0,1,2,3,4,5,6));

arguments特殊类数组

有一特殊类数组arguments可以被函数访问,它是JS最早实现任意数量参数的方法

function sum(intial){
    for(let arg of arguments)intial += arg;
    return intial;
}
console.log(sum(0,1,2,3,4,5,6));

可以通过callee属性获取arguments指向当前执行的函数,length属性表明传递给函数的数量。但目前可以忽略这一语法了,完全可以通过剩余参数语法来实现

此外箭头函数除了不支持this外,也不支持arguments

函数名

可通过Function.prototype.name获取函数名以及Function.prototype.length获取参数(剩余参数不算)的数量

Function.prototype.toString()可以直接获取某个函数源代码的字符串

有时函数名是被隐藏的,可以通过name属性获取其真实的函数名

let object = {
  someMethod: function object_someMethod() {}
};

console.log(object.someMethod.name);
// "object_someMethod"

Spread语法

实际调用可能会非常麻烦,Spread语法针对调用提供了一种便捷的语法糖,它可以将可迭代对象展开至参数中

let arr1 = new Set([1,2,3,4,5,6]);
let arr2 = [9,10,11];
//除了数组外Set,Map等可迭代对象也可以
function sum(intial , ...args){
	for(let arg of args)intial += arg;
	return intial;
}
console.log(sum(0,...arr1,7,8,...arr2,12));
//可以与普通方式混用

Spread语法除了用于调用函数外,还可作为其他任何可迭代对象的操作。至此可迭代对象除了for-of、解构赋值外还有Spread语法。

let arr1 = new Set([1,2,3,4,5]);
let arr2 = [...arr1];
//如Set复制到Array内
//Spread语法可实现浅COPY

变量作用域

变量若只在代码块或函数内声明,则变量只在该作用域内生效

{
    let a = 5;
	{
		let a = 3;
        //不同作用域同名也可
	}
}
console.log(a);//未定义

JS中每个函数、代码块、脚本...都有内部隐藏的关联对象——词法环境(Lexical Environment)。词法环境由环境记录(存储所有局部变量、属性以及其他信息)和对外部词法环境的引用(与外部代码关联)组成。若为全局词法环境,则不存在引用

函数运行时,会自动创建一个新的词法环境,在此词法环境内记录了函数内部的变量,当函数执行完毕时即销毁词法环境和连同词法环境一起的内部变量

访问一个变量时,它会优先搜索内部词法环境,若内部不存在,搜寻外部,直到全局语法环境,若全局语法环境未存在可能报错或创建一个变量

所谓闭包是指函数可以访问外部变量,JS中除了new Function都是闭包的。通过闭包可以实现函数柯里化(Currying),在本章结尾会讨论柯里化

let死区

如下代码,它实际上会报错,因为func词法环境直到调用x变量前都没有声明。虽然按照词法环境它应当调用外部的x,但由于内部词法环境尚未初始化,虽然存在无法调用外部。这也称为死区。即调用外部变量的前提是内部有初始化

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

嵌套函数

函数内部也可以嵌套函数,或者return执行函数。其中内部函数的作用域也同样被局限在内部,不影响外部

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

console.log( counter() ); // 0
console.log( counter() ); // 1
console.log( counter() ); // 2
function inBetween(a , b){
    //函数生函数
	return (x)=>{
    	return x>=a && x<= b;
    };
}
let arr = [1, 2, 3, 4, 5, 6, 7];

console.log( arr.filter(inBetween(3, 6)) );

var

JS目前标准已不推荐使用var,在某些情况下会产生奇怪的bug

1.var不具备块级作用域,var的变量直接作用域全局作用域,即在globalThis对象下。这个特性导致一些变量不能被及时释放掉,如for循环临时定义的变量,且和一些延时类函数容易出bug

2.var允许重复声明,导致你声明一个变量名相同的变量不产生报错从而可能导致一些难以发现的bug

3.var会自动提升变量,虽然可以避免let死区的问题,但某种意义上它的逻辑更加复杂,如下

let x = 1;

function func() {
  console.log(x); // undefined

  var x = 2;
}

func();

命名函数表达式NFE

NFE(Named Function Expression)如下,它自带一个名字,可以通过name属性得到其本来的函数名

let add = function func(a,b) {
  return a+b;
};
func(3,5);//报错

NFE有两个特征:1.占用空间使得无法被垃圾回收; 2.其原本的函数只允许内部进行调用无法外部调用

关于第一点

let Add = add;
add = null;

Add(3,5);
//若未采用NFE,add设置为null时
//GC就会开始回收该函数
//导致Add也被回收
//NFE可以保证改变函数名也不回收

new Function

new Function(functionBody)
new Function(arg0, functionBody)
new Function(arg0, arg1, functionBody)
new Function(arg0, arg1, /* … ,*/ argN, functionBody)

Function(functionBody)
Function(arg0, functionBody)
Function(arg0, arg1, functionBody)
Function(arg0, arg1, /* … ,*/ argN, functionBody)

一种比较少用的创建函数的语法且存在安全问题,一般只出现在特殊场景。这类函数的参数均是字符串,至少有一个且最后的参数用于接收函数体

let sum = new Function('a', 'b', 'return a + b');

alert( sum(1, 2) );

Function的词法环境指向了全局词法环境

function getFunc() {
  let value = "test";

  let func = new Function('alert(value)');

  return func;
}

getFunc()(); // error: value is not defined

Function由于使用字符串,参数可以有两种表述方式

new Function("a , b" , "return;");
new Function("a" , "b" , "return;");

最朴素的异步

setTimeout(func [, delay, arg1, arg2, ...])方法允许等待delay毫秒后再执行函数func,其中函数的参数附带在delay后,delay默认为0

setInterval(func, [delay, arg1, arg2, ...])方法允许每delay毫秒后执行依次func,即周期执行

此外定义了定时器可以通过clearTimeout(timeoutID)、clearInterval(intervalID)方法分别清除Timeout、Interval定时器以暂停。虽然Timeout、Interval是共用一个编号池,但建议不同类型定时器用不同方法清除

现在已经有方法可以计划执行代码以及暂停了。然而更常用的是嵌套定时器以完成更复杂的功能。网络中假设需要执行多次某个任务,且必须一个个严格按照顺序执行。如果只是用setInterval每100ms执行一次,那么另外一个因素就是不得不考虑任务的执行时间。setInterval不会考虑任务的执行时间会定点执行一次,比较好的情况是任务的执行时间非常短,但下一任务执行时两个任务时间间隔是小于100ms的。如果因为网络问题出现了波动,任务的执行时间可能会达到3秒,那么在此之前3秒内还会执行30次,导致任务执行完成是乱序的,显然无法完成顺序的要求。

如下图,若func1执行时间非常长,它并不会影响func2、func3...的任务执行(即乱序)

此时需要使用setTimeout,如下图,它必须保证代码执行完成后等待固定时间才会继续执行

//形式1
let i = 0;
let Func = function () {
	let timer = setTimeout(function() {
		//某个任务的代码
		i++;
		console.log(i);
		Func(); //嵌套执行
		}, 100);
	if (i >= 5) {
		//达成某任务中止条件
		clearTimeout(timer);
	}
};
Func();

//形式2
let i = 0;
let Func = function(){
	let timer = setTimeout(function f(){
    	i++;
        console.log(i);
        if(i < 5)setTimeout(f);
        //只有满足条件时才嵌套
    });
}

在浏览器环境中经过五层嵌套的定时器时间间隔至少要4毫秒(备注:来自于HTML的规定),下面代码可自行验证,浏览器环境中两任务相差时间差从第四项开始稳定保持4ms以上

let start = Date.now();
let times = [];
setTimeout(function run() {
    times.push(Date.now() - start);
    if (100 < times.slice(-1)[0]){
        console.log("累积时间差:"+times);
        console.log("两任务相差时间差:"+(times.map((v,i,arr)=>v-arr[i-1])).slice(1));
    }
    else setTimeout(run);
});

定时器可以完成最简单的异步,但它的延时都是不确定的,后面会介绍JS其他的异步编程方式

装饰器模式(decorator)

已知一个函数,现在需要对该函数的功能进行扩展,那么有这样一个装饰器可以在原有基础上直接附加功能而不用改变原有代码结构

function func(a,b){
    //求平方和
	console.log(a**2+b**2);
}

//若现在原有功能基础上再求平方差
//原做法:重写
/*
function func(a,b){
    console.log(a**2-b**2)
	console.log(a**2+b**2);
}
*/
//装饰器做法
function decorator(func){
	return function(a,b){
    	console.log(a**2-b**2);
        func(a,b);
    }
}
func = decorator(func);
func(4,3);

实际情况会更加复杂,甚至为了衍生多个功能可能出现装饰器嵌套的情况,看上去更复杂,实际上更容易维护代码。若有个非常复杂的功能再加上一个非常复杂的功能,若要重构代码是非常低效率的选择,装饰器可以避免。但这还是存在问题

let Obj = {
	a: 4,
	b: 3,
	func: function(){
		console.log(this.a ** 2 + this.b ** 2);
	},
};
function decorator(func) {
	return function () {
		console.log(this.a ** 2 - this.b ** 2);
		func();
	};
}
Obj.func = decorator(Obj.func);
Obj.func();
//func内的this无法定位至Obj

对于this无法定位的问题

Function.prototype.call(thisArg [, arg1, arg2 , ...] )可以解决

call方法可以指定一个this值来调用

let Obj = {
	a: 4,
	b: 3,
	func: function(){
		console.log(this.a ** 2 + this.b ** 2);
	},
};
function decorator(func) {
	return function () {
		console.log(this.a ** 2 - this.b ** 2);
		func.call(this);
        //指定this和当前函数一致
	};
}
Obj.func = decorator(Obj.func);
Obj.func();

另外关注到装饰器所装饰的函数参数数量不一致会导致return函数内的参数数量也不同,可以利用Rest语法。如下图一个更加完美可容纳任意参数数量的装饰器构造好了

function func1(...arg){
	/*某个功能*/
    //console.log("func1");
}
function func2(a,b,c,d){
	/*某个功能*/
    //console.log("func2");
}

function decorator(func){
	return function(...args){
    	/*某个附加功能*/
        //console.log("another");
        func.call(this,...args);
    }
}
func1 = decorator(func1);
func2 = decorator(func2);

Function.prototype.apply(thisArg [, argsArr] )是call的一个语法糖,针对任意数量的参数可简化为argsArr即(类)数组来实现,那么上述装饰器可改成如下代码(下段代码与上段的主要区别是剩余参数可以是任何可迭代对象,apply只接受类数组)

function func1(...arg){
	/*某个功能*/
    //console.log("func1");
}
function func2(a,b,c,d){
	/*某个功能*/
    //console.log("func2");
}

function decorator(func){
	return function(...args){
    	/*某个附加功能*/
        //console.log("another");
        func.apply(this,args);
    }
}
func1 = decorator(func1);
func2 = decorator(func2);

当所有参数及其上下文传递至另一函数时称为“呼叫转移”(call forwarding),call、apply其中一个作用便是如此

另外一个作用——“方法借用”(method borrowing)它可以使得某个对象借用其他对象自己所没有的方法

//通用形式
//obj2借用obj1的方法method
//参数为argsArr
obj1.method.apply(obj2,argsArr);

归功于这种技巧可以作非常多的事

function Monster(name){
    if(!new.target){
    	return new Monster(name);
    }
    this.name = name;
    this.attack = function(){
        console.log(`${this.name} is attacking`);
    }
}
function Player(name){
    if(!new.target){
    	return new Player(name);
    }
    this.name = name;
}
let m = Monster("slime");
let p = Player("John");
m.attack.call(p);
//p不具备m的方法却可以调用

装饰器于防抖节流

装饰器在JS的一个应用就是防抖和节流功能,它被封装在Lodash库中了,这两个可以通过时间来降低函数执行次数

防抖(debounce)简单来说就是装饰器套一层类似于沙漏功能的装置。要调用该函数必须等待沙漏漏完才能触发,如果中间过程再触发相当于重置沙漏

let time = 1000;
let f = debounce(console.log, time);
//假设f是某个触发事件
//time秒后f事件不再触发后才执行函数console.log

function debounce(func, time) {
	let timer;
	return function (...args) {
		clearTimeout(timer);
         //实际代码会加入if
         //博主认为并不需要
		timer = setTimeout(() => {
			func.apply(this, args);
		}, time);
	};
}

防抖是停止调用后执行一次函数,而节流(throttle)会对时间分段,某段时间只执行一次。节流是针对不同的需求,但不管是什么需求,它的一个前提要检测次数次数密集,但检测后要执行的函数次数却没有那么多

function throttle(func, time){
	let start = 0;
    return function(...args){
    	let now = Date.now();
		if(now-start < time){
			return;
		}
         start = now;
		func.apply(this, args);
    }
}

绑定函数(bound function)

JS中的this会一直发生变化,很难保证this作用到正确的对象中,虽然可以用箭头函数但也不能完全解决

let user = {
	firstName: "John",
	sayHi() {
		console.log(`Hello, ${this.firstName}!`);
	},
};
setTimeout(() => user.sayHi(), 1000);
//额外的包装器保证this指向user
//但user发生了变化
//包装器的原理是this扩展至全局语法环境
//全局语法环境可寻找到user
user = {
	sayHi() {
		console.log("Another user in setTimeout!");
	},
};

function.bind(thisArg [ , arg1 , arg2, ... ] )方法将会返回一个绑定this的函数,this不会因为函数调用发生变化

let user = {
	firstName: "John",
	sayHi() {
		console.log(`Hello, ${this.firstName}!`);
	},
};
setTimeout(user.sayHi.bind(user), 1000);
user = {
	sayHi() {
		console.log("Another user in setTimeout!");
	},
};

关于绑定函数的用法Lodash库中提供了更多,这里不作具体介绍。bind还可以绑定参数即固定参数,虽然应用比较少。另外一个是绑定函数无法被重绑定(re-bound)

function f() {
  console.log(this.name);
}
f.bind( {name: "John"} ).bind( {name: "Pete"} )(); // John

箭头函数

■箭头函数不具备this

■箭头函数不具备arguments

■箭头函数无法new(即无法创建函数构造器)

■箭头函数不具备super(类的语法)



五、Object类型

构造Object

构造Object可以使用new Object来实现,它接受任意值,如果输入null、undefined将会返回空对象,否则会返回与给定值相关的对象,也会将一些基础类型变成相应的包装类型,另外Object.create有其他用法之后会说明

属性(Property)

Object中的属性不仅仅是键值对的用法,它有更强的功能

属性存在三类特殊标志,均是布尔类型——writable、enumerable、configurable

writable若为true表示这个属性可以被修改,否则表示不可修改为只读属性

enumerable若为true表示可枚举会被循环中列出

configurable若为true表示可以删除属性且可以修改标志

属性标志只能通过Object的一些方法来做相应操作

Object.getOwnPropertyDescriptor(obj , name)方法可以获取obj对象中name属性的所有内容,返回一个value、writable、enumerable、configurable、set、get属性的对象(关于set、get涉及到另外一块内容,下面会讲到)。根据如下代码属性一开始默认所有标志为true即可写入、可枚举、可删除

备注:在ES2015,如果第一个参数不是Object将会自动转成Object类型

let b = Symbol("b");
let obj = {
	a: 1,
    [b]: 2
}
console.log(Object.getOwnPropertyDescriptor(obj,"a"));
console.log(Object.getOwnPropertyDescriptor(obj,b));
console.log(Object.getOwnPropertyDescriptor(obj,"c"));
//如果找不到会返回undefined

如果觉得这样子太麻烦可以尝试另外一个方法

Object.getOwnPropertyDescriptors(obj , name1 , ...)功能和上个方法一致,但可接收任意数量,返回一个属性名为输入的name,值为value、writable、enumerable、configurable、set、get属性的对象的对象

let b = Symbol("b");
let obj = {
	a: 1,
    [b]: 2
}
console.log(Object.getOwnPropertyDescriptors(obj,"a",b,"c"));
//如果不存在属性会返回空对象

Object.defineProperty(obj , prop, descriptor)可以定义或修改obj中的prop属性为descriptor,其中descriptor参考getOwnPropertyDescriptor的格式(备注:返回修改后的obj可支持链式编程)

let obj = {a: 1};
let b = Symbol("b");
Object.defineProperty(obj,"a",{value: 10,writable: false});
Object.defineProperty(obj,b,{value: 2,configurable: false});
console.log(obj);
obj.a = 1;//无法修改
delete obj.b;//无法删除
console.log(obj);
//备注:在非严格模式下,违反标志的操作会直接忽略
obj.b = 20;//无法修改
console.log(obj);
//必须完整写好哪些是true是false

同理也有Object.defineProerties(obj, props)简化操作

let obj = { a: 1 };
let b = Symbol("b");
Object.defineProperties(obj, {
	a: { value: 10, writable: false },
	[b]: { value: 2, configurable: false },
});
console.log(obj);
let obj = {
	a: 1,
	b: 2,
	f: function(){
		console.log(this);
	}
};
for(let [key,value] of Object.entries(obj)){
	console.log("key:"+key+",\tvalue:"+value);
}
//由于f是可枚举的
//若尝试枚举对象
//导致函数也会被枚举到
//实际情况只会想枚举属性
let obj2 = {
	a: 1,
    b: 2
};
Object.defineProperty(obj2,"f",{
    value: function(){console.log(this);},
    writable: true,
    enumerable:false,
    configurable:true
});
for(let [key,value] of Object.entries(obj2)){
	console.log("key:"+key+",\tvalue:"+value);
}

configurable是指不可配置,一旦设置为false,这个属性不可删除也无法通过相关方法修改定义,如Math.PI它只读、不可枚举、不可配置,不仅无法修改相关值也无法通过Object.defineProperty修改。但并非完全不可配置,若writable为true可以修改为false

回看下Object.assign方法,当时这个方法只是简单说了下是一个浅COPY甚至不如JSON组合的深COPY好。它更具体的描述是它只会浅COPY所有可枚举的属性(还有一个是自有属性的要求,和原型链相关)

JSON方法只是字符串转成对象,显然针对属性标志是无法做相关处理的

let obj2 = {
	a: 1,
    b: 2
};
Object.defineProperty(obj2,"f",{
    value: function(){console.log(this);},
    writable: true,
    enumerable:false,
    configurable:true
});
let obj3 = {};
console.log(Object.assign(obj3,obj2));
console.log(JSON.parse(JSON.stringify(obj3)));
//可以发现不管是assign、JSON都是无法处理属性标志
//COPY当然是得要全COPY
//这么来看要实现Object的COPY还挺复杂

限制对象

此外JS提供了三个不同级别的限制方法用于限制对对象的一些操作,但这些限制只针对自有属性,而不针对原型链的属性

第一级别——不可扩展:Object.preventExtensions(obj)将会让obj无法再添加自有属性。此外Object.isExtensible(obj)用于判断是否是可扩展的

let obj1 = {};
Object.preventExtensions(obj1);
obj1.a = 1;
console.log(Object.isExtensible(obj1));
console.log(obj1);

第二级别——密封:Object.seal(obj)将会让obj无法再添加、删除自有属性。Object.isSealed(obj)方法用于判断是否是密封的。密封主要在不可扩展的基础上每个属性添加configurable: false

let obj2 = {a: 1};
Object.seal(obj2);
obj2.b = 2;
delete obj2.a;
console.log(Object.isSealed(obj2));
console.log(obj2);

第三级别——冻结:Object.freeze(obj)将会让obj无法再添加、删除、修改自有属性。Object.isFrozen(obj)方法用于判断是否是冻结的。冻结主要在密封的基础上每个属性添加writable: false

let obj3 = {a: 1};
Object.freeze(obj3);
obj3.b = 2;
obj3.a = 10;
delete obj3.a;
console.log(Object.isFrozen(obj3));
console.log(obj3);

三个级别的限制层层递进,但一般挺少用到的,可能会用在一些API上以保证API不可以被程序员轻易篡改

访问器属性

至此为止所接触的属性(包括函数)都称为数据属性,但有另外一类称为访问器属性(accessor property)

访问器属性是当用户读取、赋值操作时触发的函数,但从外部来看像普通属性一样,用getter、setter表示

let obj = {
  get propName() {
    // 当读取 obj.propName 时,getter 起作用
  },
  set propName(value) {
    // 当执行 obj.propName = value 操作时,setter 起作用
  }
};
let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};
user.fullName = "Alice Cooper";
//若无setter该语句会报错
//只有赋予setter后才能赋值
console.log(user.name); // Alice
console.log(user.surname); // Cooper

对于数据属性而言它有value、writable、enumerable、configurable,对于访问器属性而言它有enumerable、configurable、get、set。其中访问器属性可以没有set相当于数据属性中writable: false变成了只读的访问器属性

let user = {
  name: "John",
  surname: "Smith"
};
Object.defineProperty(user, "fullName", {
	get() {
    	return `${this.name} ${this.surname}`;
  	},
    set(value) {
    	[this.name, this.surname] = value.split(" ");
  	},
    enumerable: true,
    configurable: true
});
user.fullName = "Alice Cooper";
console.log(user.name);
console.log(user.surname);

博主认为访问器属性最大好处就是每次读取、赋值时可以做预处理,只有被允许的数据才能写入

备注:不作任何处理直接COPY会将访问器属性变成普通属性不具备setter、getter

原型继承

JS中对象具备特殊的隐藏属性[[Prototype]],该值要么为null要么为某个对象的引用,被引用的对象也称为“原型”

当对象读取某个缺失的属性时它会从原型去寻找,当然如果原型也没有那么会从原型的原型去寻找。用面向对象的话来说原型就是父对象,而且是像Java那样的一对多继承。其中如果属性不是原型的称为是自有属性

有非常多的方法去访问原型,但大部分方法都不被推荐,只有Object.create被推荐作原型相关的处理

方法1——__proto__:可以简单的通过该属性直接访问或赋值原型。原型可以实现重写但无法实现重载,毕竟JS可以接受任意参数,若缺失视为undefined,若多余也没什么问题

let animal = {
  eats: true,
  walk:function(){console.log("Animal is walking");}
};
let rabbit = {
  jumps: true,
  walk:function(){console.log("Rabbit is walking");}
};

rabbit.__proto__ = animal;
console.log(rabbit.eats);
rabbit.walk();

赋值与读取的差异:如下代码, (*)可能被误解为修改user的test,但实际上它是直接修改对象的属性,只有读取时不存在才会去访问原型

let user = {
  name: "John",
  surname: "Smith",
  test: "123"
};
let admin = {
  __proto__: user,
  isAdmin: true
}; 
admin.test = "test";// (*)
console.log(admin.test);//test
console.log(user.test);//123

原型上访问器属性的情况:针对于访问器属性,如下代码,在一开始对象就会拥有setter执行后的fullName且因为尚未拥有name、surname属性直接调用user的name、surname,而调用getter时此时因为具备了name、surname直接套用当前对象导致admin、user完全不同。若修改this为user又会变成另外一种情况

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};
admin.fullName = "Alice Cooper";
console.log(admin);
console.log(user);

JS中的this只看当前调用的上下文不会像面向对象语言那样还考虑父对象

for-in循环也会去循环原型的键值对,若只想循环自有属性而非原型属性,可以尝试hasOwnProperty方法,但即使这样也只是过滤原型属性,建议使用keys、value或entries方法以剔除原型属性的影响

let obj1 = {0:0,1:1,2:2,3:3};
let obj2 = {4:4,5:5,6:6,7:7,8:8,__proto__:obj1};
for(let i in obj2){
    //for-in循环会去循环原型
    //备注:for-in循环只会循环可枚举的
    //__proto__是不可枚举的
    console.log(i);
}

从性能角度来说,一个足够长的原型链,直接从原型获取属性或是从对象通过原型链获取属性并没有性能上的差异,引擎会自动优化

方法2——Object.getPrototypeOf(obj)以及Object.setPrototypeOf(obj, prototype)用于获取、设置原型链。另外一个方法Object.prototype.isPrototypeOf(obj)用于判断当前对象是否在obj的原型链上。但这个方法也不推荐,它虽然比方法1好些但也存在性能问题

let obj1 = {0:0};
let obj2 = {1:1};
Object.setPrototypeOf(obj2,obj1);
console.log(Object.getPrototypeOf(obj2));

构造函数

每一函数都有prototype属性,prototype在new对象时设置[[Prototype]],默认情况下函数的prototype都是属性constructor指向函数自身的对象。constructor是对象的属性,可以获取它的构造函数

let obj1 = {0:0};
function Obj2(){this["1"]=1;}
Obj2.prototype = obj1;
let obj2 = new Obj2();
console.log(obj2);
//obj2原型为obj1

虽然prototype可以修改原型链,但另外一个问题是constructor也发生了改变,如果从已知的对象去获取构造函数并不能正确地获取

function Obj(){this["0"] = 0;}
Obj.prototype = {1:1};
let obj = new Obj();
console.log(obj.constructor === Obj);//false

一个比较好的方法是在设置prototype的同时也去设置constructor,可获取constructor后除了可构造同等类型的对象,另外一个就是通过constructor再去改原型链的对象

function Obj(){this["0"] = 0;}
Obj.prototype = {1:1,constructor:Obj};
let obj = new Obj();
console.log(obj.constructor === Obj);//true
let obj2 = new obj.constructor();
//现在可以通过constructor来构造同类型的Object
console.log(obj2);

delete obj.constructor.prototype["1"];
//再去修改原型链

在JS内部的各个内建对象有非常复杂的原型链关系,但最终Object.prototype是最终的原型,Object.prototype.prototype为null。由此可以参考JS的实现方式,构造函数生成某个类型的对象,其原型设计为某个存储大量方法、特殊属性的对象。实际上参考MDN文档的过程中会遇到大量prototype的方法,它可以被某个类型的实例对象直接使用,因为实例对象可以通过原型链获取,而另外的没有设计prototype方法无法通过原型链获取,如Math.PI无法通过1.PI去获取

大部分JS语法的根基都是对象。但另外的基本数据类型(虽然有相应的Object包装器)不是对象。其中JS内建类型的原型也可以修改,比如可以修改Math.prototype设计一个Math.prototype.gcd来计算最大公约数。但修改原生原型并不是一个好的想法,比如命名出现冲突,只有polyfill时才是比较有用的

方法3——Object.create(proto , [ proerties ] ),直接设置原型存在性能问题,而反过来根据原型设置对象性能更优,create方法将会创建一个以proto为原型的空对象。第二个参数proerties用于设置创建空对象的自由属性,其语法和getOwnPropertyDescriptors一致

function InfityNumber(max,min=0){
	if(max<=0 || min<0 ||  || typeof max != "number" || typeof min != "number")
}

博主编写了一个案例,来自于Chris Crawford的有限数值模型以及博主自作的一些优化

////InfinityNumber: 有限数值类型
//范围局限于[-1,1]
//有特殊运算
function InfinityNumber(value = 0) {
    if (!new.target) return new InfinityNumber(value);
    if (typeof value !== "number" || value < -1 || value > 1) {
        return undefined;
    }
    this.value = value;
    Object.defineProperties(this, {
        [Symbol.toStringTag]: {
            get() {
                return "InfinityNumber";
            },
        },
    });
}
InfinityNumber.prototype = {
    constructor: InfinityNumber,
    getValue() {
        //获取数据
        return this.value;
    },
    add(value) {
        //加法
        return !InfinityNumber.is(value)
            ? this
            : InfinityNumber(
                  this.getValue() +
                      value.getValue() *
                          Math.abs(
                              Math.sign(value.getValue()) - this.getValue()
                          )
              );
    },
    controlAdd(value, ...points) {
        //精确加法
        if (!InfinityNumber.is(value)) return this;
        for (let i of points) {
            if (!InfinityNumber.is(i)) return this;
        }
        points.unshift(InfinityNumber.min);
        points.push(InfinityNumber.max);
        for (let i in points) {
            if (this.getValue() <= points[+i + 1].getValue()) {
                if (+i + 1 == points.length - 1) i--;
                return InfinityNumber(
                    this.getValue() +
                        value.getValue() *
                            Math.abs(
                                Math.sign(value.getValue()) *
                                    points[+i + 2].getValue() -
                                    this.getValue()
                            )
                );
            }
        }
    },
    map(func) {
        //实现数值映射
        return func(this.getValue());
    },
};
//InfinityNumber最小值
InfinityNumber.min = InfinityNumber(-1);
//InfinityNumber最大值
InfinityNumber.max = InfinityNumber(1);
//判断是否为InfinityNumber
InfinityNumber.is = function (value) {
    return value?.toString() == "[object InfinityNumber]" ? true : false;
};
//冻结InfinityNumber防止被随意篡改
Object.freeze(InfinityNumber);
//END

function convert1(x) {
    return x * 5000 + 5000;
}
function convert2(x) {
    return (x - 5000) / 5000;
}
let i1 = InfinityNumber(-0.9);
let i2 = i1.constructor(0.12);
let points = [InfinityNumber(-0.5), InfinityNumber(), InfinityNumber(0.5)];

let arr = [i1.map(convert1)];
for (let i = 0; i < 15; i++) {
    i1 = i1.controlAdd(i2, ...points);
    arr.push(Math.floor(i1.map(convert1)));
}
console.log("数据:" + arr);
let arr2 = arr.map((v, i) => arr[i + 1] - v);
arr2.pop();
console.log("变化量:" + arr2);

Very plain对象(pure dictionary)

一个比较***钻的问题如下代码,key值恰好原型的话将会忽略

let obj = {};
obj["__proto__"] = "test";
console.log(obj.__proto__);

如果查看Object的原型的话它会有很多方法、属性,而且原型有set、get函数即原型是一个访问器属性。

为了解决这个问题,有一类对象它不存在原型,这样的对象称为Very plain或pure dictionary对象,这类对象没有任何提前内置的方法,是更加简洁的对象,但同时也不具备constructor、toString等原先内置的属性、方法。可以根据需求来选择

let obj = Object.create(null);
obj["__proto__"] = "test";
console.log(obj.__proto__);

Object属性、方法总览

■构造器

Object.prototype.constructor获取当前对象的构造函数

■浅COPY

Object.assign(target, ...sources) 浅COPY可枚举的自有的属性

■原型链相关

Object.create(proto [, properties ] )根据原型链创建对象,第二个参数用于设定自有属性

Object.getPrototypeOf(obj)返回obj的原型对象

Object.setPrototypeOf(obj, prototype)设置obj的原型prototype

prototypeObj.isPrototypeOf(obj)判断当前对象是否在obj的原型链上

■判断

Object.is(value1, value2)将会判断是否为同一个值(备注:与等于不同,只判断值或对象的引用是否相同,如NaN可以看成一样,+0、-0不同)

Object.hasOwn(instance, prop)会判断instance是否拥有prop属性(prop为String或Symbol类型)

Object.prototype.hasOwnProperty(prop)功能同上,但不建议使用,若遇到Very plain对象只能使用上面的方法

Object.prototype.propertyIsEnumerable(prop)判断属性prop是否可枚举

■获取属性集

Object.getOwnPropertyNames(obj)返回obj的所有非Symbol且是自有的属性名的数组

Object.getOwnPropertySymbols(obj)返回obj所有Symbol且是自有的Symbol属性的数组

■获取、定义属性

Object.getOwnPropertyDescriptor(obj, prop)获取obj对象的关于prop属性的描述符对象

Object.getOwnPropertyDescriptors(obj)获取obj对象的所有属性的描述符对象

Object.defineProperty(obj, prop, descriptor)定义或修改obj的单个属性

Object.defineProperties(obj, props)定义或修改obj的属性

■迭代方法

Object.keys(obj)返回obj自有的可枚举的所有属性名的数组

Object.values(obj)返回obj自有的可枚举的所有属性值的数组

Object.entries(obj)会返回obj的键值对数组(二元素)

Object.fromEntries(iterable)将可迭代对象(且具备二元素的)转成普通对象

■限制属性

Object.preventExtensions(obj)让obj对象不可扩展

Object.isExtensible(obj)判断obj是否可扩展

Object.seal(obj)密封obj

Object.isSealed(obj)判断obj是否密封

Object.freeze(obj)冻结obj

Object.isFrozen(obj)判断obj是否冻结

■其他

Object.prototype.toString()返回表示对象的字符串

Object.prototype.valueOf()返回this

Object.prototype.toLocaleString()同toString主要是给其他对象(Array、Number、Date)提供通用方法,和i8n相关

备注:和运算环境相关,为了让object也能参与数字、字符串环境运算,一般需要重写



参考资料

[1] 《JavaScrpit DOM 编程艺术》

[2] MDN

[3] 现代JS教程

[4] 黑马程序员 JS pink

posted on 2023-01-05 09:57  摸鱼鱼的尛善  阅读(55)  评论(0编辑  收藏  举报