1、Object(this)
<script>
// 接下来要说的这个很关键,一直没看懂,今天终于看懂了
// 今天把它放在第零项就是要告诉自己,所有的东西存在一定有它的意义,努力弄懂它
// 好!首先我们抛出问题~
// 为什么我们需要做 var o = Object(this); 那么Object(this)到底帮助实现了什么?
// 答案:在严格模式下,一个原始的this将不会被强制转换成一个对象。因此,使用Object(this)对对象进行显式强制转换是必要的,转换后才能当作对象访问
// 我们来看下下面的对比的例子
const array = Array.prototype;
Object.defineProperty(array, 'foo1', {
value() {
return this.length >>> 0;
}
});
Object.defineProperty(array, 'foo2', {
value() {
"use strict";
return this.length >>> 0;
}
});
console.log(Array.prototype.foo1.call(undefined));
console.log(Array.prototype.foo2.call(undefined));
// 第一个示例运行成功,结果为 0,因为参数undefined在非严格模式下被强制转换为一个对象,
// 第二个例子失败了,因为undefined在严格模式下没有被强制转换,因此this.length出错
// 不知道为啥,2023年11月20日我再看这个发现我靠我看不懂了,于是我又开始看了一下
// 在看filter实现的源码中,我们可以发现const O = Object(this);
Array.prototype.filter = function (callback, thisArg) {
// console.log(null == undefined) true
// console.log(null === undefined) false
if (this == undefined) {
throw new TypeError('this is null or not undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + 'is not a function');
}
const res = [];
const O = Object(this);
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {
// 检查索引 i 是否是 O 的一个有效属性。这是为了防止在稀疏数组中处理不存在的元素。
// 可以参照下面这个注释的例子
// const arr = [];
// arr[5] = 10;
// console.log(arr);
// console.log(5 in arr); // true
// console.log(3 in arr); // false
if (i in O) {
if (callback.call(thisArg, O[i], i, O)) {
res.push(O[i]);
}
}
}
return res;
}
// 你说这里为啥要用Object(this)转一下呢,后来啊发现,我们正常用数组的话,那this就是数组
// 可是如果我们用string字符串呢,会变成什么样的呢?可是字符串又不能调用数组方法,那又怎么办呢?
// 首先我们用Reflect.apply可以使字符串调用数组方法,这就是反射的厉害,下节我们再说,
// 然后我们看下,我们发现this被强转成了string对象,好像也没啥问题,O也是string对象,好像这一步还是
// 多余
Array.prototype.filter = function (callback, thisArg) {
if (this == undefined) {
throw new TypeError('this is null or not undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + 'is not a function');
}
const res = [];
console.log(this) // String {"4"}
const O = Object(this);
}
Reflect.apply(Array.prototype.filter, 'abc', [(item) => {
return item
}])
Reflect.apply(Array.prototype.filter, 123, [(item) => {
return item
}])
// 这个时候就出来了,上卖弄那个默认是非严格模式,现在我们开启严格模式
// 在这里我使用了基本数据类型作为this的指向,但打印出来的this竟然是个对象
// 这就有点奇怪了,后来我查了资料才发现,在非严格模式下改变this指向时,this都会被
// 包装成一个对象,但在严格模式下则不会包装。这下就明白了,我将use strict加
// 上后果然this变成了基础数据类型了
Array.prototype.filter = function (callback, thisArg) {
'use strict'
if (this == undefined) {
throw new TypeError('this is null or not undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + 'is not a function');
}
const res = [];
console.log(this) // 4
// 这个时候就必须要用下面这句话了
// 不然后面的代码走不通,string不可以用in遍历
// const O = Object(this);
// const len = O.length >>> 0;
// for (let i = 0; i < len; i++) {
// if (i in O) {
// if (callback.call(thisArg, O[i], i, O)) {
// res.push(O[i]);
// }
// }
// }
// return res;
}
Reflect.apply(Array.prototype.filter, 'abc', [(item) => {
return item
}])
// 为了验证这一句话:在非严格模式下改变this指向时,this都会被包装成一个对象
// 我写了下面这个代码,你发现打印的this成对象了,而不是字符串
Array.prototype.myFilter = function() {
console.log(this);
};
[].myFilter.apply('abc');
</script>
// 20240409:补充一下文心一言的回答
// 在代码中,const O = Object(this); 这句话的作用是将 this 转化为一个对象。这是为了确保 this 引用的是一个可以被遍历的类数组对象,即使它原本不是一个对象。
// 这里的 this 指向调用 filter 方法的数组或类数组对象。在 JavaScript 中,数组本质上也是对象,但是 this 有可能是 undefined 或其他非对象类型的值。为了确保后续代码可以安全地处理 this(例如,访问其 length 属性或进行遍历),我们首先使用 Object() 函数将其转化为一个对象。
// 这样做有几个好处:
// 安全性:如果 this 是 null 或 undefined,那么 Object(null) 或 Object(undefined) 会分别返回一个新的空对象 {}。这样,我们可以在后续代码中避免 TypeError。
// 通用性:Object() 函数还可以处理其他非对象类型的值,例如数字、字符串或布尔值。虽然这些类型在 filter 方法的上下文中可能不太常见,但这种转换确保了代码的通用性和健壮性。
// 一致性:将 this 转化为对象后,我们可以一致地访问其属性(如 length)和使用 for 循环遍历其元素,无论 this 最初是什么类型。
// 下面这个是GPT4的回答
// 在这段代码中,const O = Object(this); 这行的作用是确保 O 是一个对象实例。这里 this 应该是一个数组,因为 filter 方法本来就是数组的一个方法。但是为了编写一个健壮的方法,作者可能想要确保无论 this 的原始类型是什么,将其转换为对象。即使 this 已经是一个对象,Object(this) 表达式也只是简单地返回 this。
// 使用 Object() 构造函数可以有以下好处:
// 兼容性:这允许 filter 方法能够处理类数组对象(例如 arguments 对象或者 DOM NodeList 对象),这些对象不是真正的数组,但它们有长度属性和可以通过索引访问的元素。
// 健壮性:如果有人以一种不是数组的方式调用 filter 方法(例如直接用一个对象调用 `Array.prototype.filter.call(someObject, …)),代码仍然能够尝试运行而不是立即失败。
// 在这里,Object(this) 保证后续操作都是在对象上执行的,所以 O.length 就能够尝试访问 this 当作对象时的 length 属性。然后,const len = O.length >>> 0; 这行代码是将 length 属性强制转换为无符号32位整数,确保 len 是一个非负整数,这对于后续的循环操作是必要的。
// 简而言之,const O = Object(this); 这行代码是出于安全和兼容性考虑,确保 filter 方法可以正常工作,即使在非数组对象上被调用时。
// 用下面这个试一试,好好看一看在非严格模式下String对象到底是什么!!!!!!!!!!!!!!!!!!!
Array.prototype.myFilter.call('wujunjie', item => {
console.log(item);
})
2、Reflect
// 1、概念
// Reflect是ES6中新增的一个内置对象,它提供了一组静态方法,用于操作对象。这些方法与Object上的
// 方法具有相同的功能。在这些方法中会调用对应Object上的方法,并且返回对应结果。Reflect的出
// 现主要是为了将一些Object对象上的方法转移到Reflect上,使得操作对象更加统一和易于理解。
// 通过这种方式,实现了对Object上方法的封装和统一。
// 2、作用
// 1、统一了操作对象的API:通过使用Reflect对象上的方法,我们可以统一和简化对对象的操作。例如,我们可以使用Reflect.get()来获取一个属性值,而不需要再使用obj[key]这种方式。
// 2、提供了默认行为:在某些情况下,我们可能需要自定义某个操作的行为。通过使用Reflect对象上的方法,我们可以在自定义行为中调用默认行为,并且不需要再手动实现默认行为。
// 3、应用场景
// 1. 属性操作:Reflect对象的方法可以用于获取、设置和删除对象的属性。它们提供了更加直观和统一的方式来操作属性,例如使用Reflect.get()获取属性值,使用Reflect.set()设置属性值,使用Reflect.deleteProperty()删除属性。
// 2. 原型操作:通过使用Reflect.getPrototypeOf()和Reflect.setPrototypeOf()方法,可以方便地获取和设置对象的原型。这对于实现继承、原型链操作等场景非常有用。
// 3. 构造函数调用:通过使用Reflect.construct()方法,可以动态地创建一个对象实例。这对于动态创建对象、实现工厂模式等场景非常有用。
// 4. 函数调用:通过使用Reflect.apply()方法,可以动态地调用一个函数,并传入指定参数。这对于实现函数调用的灵活性和可扩展性非常有帮助。
// 5. 属性描述符操作:通过使用Reflect.defineProperty()方法,可以定义或修改属性的属性描述符。这对于控制属性特性(如可写性、可枚举性、可配置性)非常有用。
// 6. 对象扩展控制:通过使用Reflect.preventExtensions()和Reflect.isExtensible()方法,可以控制对象是否可扩展。这对于限制或控制对象是否能够添加新属性非常有帮助。
// 7. 代理对象操作:Reflect对象的方法在使用代理对象时非常有用。通过使用Reflect对象的方法,可以在代理对象的处理函数中调用默认行为,实现更加灵活和可控的代理操作。
// 4、API
4.1、Reflect.apply(target, thisArg, args)
// 作用:调用一个函数,并传入指定参数
// 参数:
// target:目标函数
// thisArg:函数执行时的this值
// args:一个数组或类数组对象,包含要传递给函数的参数
// 示例:
function sum(a, b) {
return a + b;
}
const result = Reflect.apply(sum, null, [1, 2]);
console.log(result); // 输出:3
4.2、Reflect.construct(target, args)
// 作用:使用指定参数创建一个对象
// 参数:
// target:目标构造函数
// args:一个数组或类数组对象,包含要传递给构造函数的参数
// 示例:
class Person {
constructor(name) {
this.name = name;
}
}
const obj = Reflect.construct(Person, ['Alice']);
console.log(obj instanceof Person); // 输出:true
console.log(obj.name); // 输出:Alice
4.3、Reflect.get(target, name, receiver)
// 作用:获取指定属性的值
// 参数:
// target:目标对象
// propertyKey:要获取值的属性名称
// receiver(可选):如果target是代理对象,则receiver是代理对象或继承自代理对象的对象,如果不是代理对象,则receiver会被忽略
// 示例:
const obj = { name: 'Alice' };
const value = Reflect.get(obj, 'name');
console.log(value); // 输出:Alice
4.4、Reflect.set(target, name, value, receiver)
// 作用:设置指定属性的值
// 参数:
// target:目标对象
// propertyKey:要设置值的属性名称
// value:要设置的值
// receiver(可选):如果target是代理对象,则receiver是代理对象或继承自代理对象的对象,如果不是代理对象,则receiver会被忽略
// 示例:
const obj = { name: 'Alice' };
Reflect.set(obj, 'name', 'Bob');
console.log(obj.name); // 输出:Bob
4.5、Reflect.defineProperty(target, name, desc)
// 作用:定义一个新属性或修改现有属性的属性描述符。
// 参数:
// target:目标对象。
// propertyKey:要定义或修改的属性名称。
// attributes:一个对象,包含要定义或修改的属性的各种特性,如value、writable、enumerable和configurable等。
// 示例:
const obj = {};
Reflect.defineProperty(obj, 'name', {
value: 'Alice',
writable: false,
enumerable: true,
configurable: true
});
console.log(obj.name); // 输出:Alice
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor.value); // 输出:Alice
console.log(descriptor.writable); // 输出:false
console.log(descriptor.enumerable); // 输出:true
console.log(descriptor.configurable); // 输出:true
4.6、Reflect.deleteProperty(target, name)
// 作用:删除对象的指定属性。
// 参数:
// target:目标对象。
// propertyKey:要删除的属性名称。
// 示例:
const obj = { name: 'Alice' };
Reflect.deleteProperty(obj, 'name');
console.log(obj.name); // 输出:undefined
4.7、Reflect.has(target, name)
// 作用:判断对象是否具有指定属性
// 参数:
// target:目标对象
// propertyKey:要判断的属性名称
// 示例:
const obj = { name: 'Alice' };
const hasName = Reflect.has(obj, 'name');
console.log(hasName); // 输出:true
const hasAge = Reflect.has(obj, 'age');
console.log(hasAge); // 输出:false
4.8、Reflect.ownKeys(target)
4.9、Reflect.isExtensible(target)
// 作用:判断对象是否可扩展。
// 参数:
// target:目标对象。
// 示例:
const obj = {};
console.log(Reflect.isExtensible(obj)); // 输出:true
Object.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // 输出:false
4.10、Reflect.preventExtensions(target)
4.11、Reflect.getOwnPropertyDescriptor(target, name)
4.12、Reflect.getPrototypeOf(target)
// 作用:获取对象的原型。
// 参数:target:目标对象。
// 示例:
const obj = {};
const proto = { name: 'Alice' };
Object.setPrototypeOf(obj, proto);
const prototype = Reflect.getPrototypeOf(obj);
console.log(prototype.name); // 输出:Alice
4.13、Reflect.setPrototypeOf(target, prototype)
// 作用:设置对象的原型。
// 参数:
// target:目标对象。
// prototype:要设置为目标对象原型的对象。
// 示例:
const obj = {};
const proto = { name: 'Alice' };
Reflect.setPrototypeOf(obj, proto);
console.log(obj.name); // 输出:Alice
const descriptor = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), 'name');
console.log(descriptor.value); // 输出:Alice
// 有一篇非常好的文章看一下的:
// https://juejin.cn/post/7080916820353351688?searchId=2024041216303265A41E3C0A5D8904061F
3、无符号右移(>>>)
<script>
// 接下来要说的这个很关键,一直没看懂,今天终于看懂了
// 今天把它放在第零点五项就是要告诉自己,所有的东西存在一定有它的意义,努力弄懂它
// 好吧抛出问题,无符号右移(>>>)到底干了什么?
// 首先我们要了解什么是移位运算符,其实就是先用二进制表示,然后利用二进制的移位实现值的改变
// 1、左移运算符(<<)
// 比如我们需要把26左移一位,那么26的二进制位是11010, 将它向左移位,一个字节都是八位的,
// 我们把左边的零补齐 就是 00011010, 整体向左移动一位,第一个零移出去了不要了,后面补上
// 零,那这个就变成了00110100,而他的十进制就是52,可以看结果
// 26左移一位
console.log(26 << 1); // 52
console.log(26 * Math.pow(2,1));
// 26左移两位
console.log(26 << 2);
console.log(26 * Math.pow(2,2));
// 通过多次结果你会得出左移的方程就是:m << n 等于 m * 2^n,负数也满足这个条件,负数的表示涉及原码和补码,自己去看吧
// 2、右移运算,右移运算分两种一种是有符号的右移运算符,另一种是无符号的右移运算符
// 2.1、有符号的右移
// 正数右移,左侧补0;负数右移,左侧补1
// 26 >> 1 也就是 00011010 变成 00001101 变成 13
// 13 >> 1 也就是 00001101 变成 00000110 变成 6,因为有一个1被移出了,所以数字不精确了,变成6而不是6.5
// 通过多次结果你会得出右移的方程就是:m >> n 等于 m / 2^n,负数也满足这个条件,但是出现小数,小数会被舍弃, 负数也满足这个条件,负数的表示涉及原码和补码,自己去看吧
// 2.2、无符号的右移
// 26 >>> 1 就是 左侧补零 正负数的无符号右移都是左侧补零
// 需要注意的是数字字符串会被转换成number并取整,普通字符串直接转成零
console.log(26 >>> 0); // 26
console.log(13 >>> 0); // 13
console.log(13.7 >>> 0); // 13
console.log(13.5 >>> 0); // 13
console.log(13.2 >>> 0); // 13
console.log('13.2' >>> 0); // 13
console.log('sdfdsf'>>> 0); // 0
console.log(0 >>> 0); // 0
console.log(-1 >>> 0); // 2147483647 (负数的原码和补码自己看)
console.log(-26 >>> 0); // 2147483635 (负数的原码和补码自己看)
console.log(-13 >>> 0); // 2147483641 (负数的原码和补码自己看)
</script>
4、手写typeof
<script>
function myTypeOf1(obj) {
return Object.prototype.toString.call(obj);
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 注意:toString没有括号!!!!!!!!!!!!!!!!!!!!!!
function myTypeOf2(obj) {
return Object.prototype.toString.call(obj).slice(8,-1).toLowerCase();
}
console.log(myTypeOf1([])); //[object Array]
console.log(myTypeOf1({})); //[object Object]
console.log(myTypeOf1(new Date)); //[object Date]
console.log(myTypeOf1(5)); //[object Number]
console.log(myTypeOf1('sdfsdfdsf')); //[object String]
console.log(myTypeOf1(true)); //[object Boolean]
console.log(myTypeOf1(null)); //[object Null]
console.log(myTypeOf1(myTypeOf1)); //[object Function]
console.log(myTypeOf1(undefined)); //[object Undefined]
console.log(myTypeOf1(NaN)); //[object Number]
console.log(myTypeOf2([])); //array
console.log(myTypeOf2({})); //object
console.log(myTypeOf2(new Date)); //date
console.log(myTypeOf2(5)); //number
console.log(myTypeOf2('sdfsdfdsf')); //string
console.log(myTypeOf2(true)); //boolean
console.log(myTypeOf2(null)); //null
console.log(myTypeOf2(myTypeOf2)); //function
console.log(myTypeOf2(undefined)); //undefined
console.log(myTypeOf2(NaN)); //number
// 我们来看看原生的typeof是怎样的结果
console.log(typeof []); //object
console.log(typeof {}); //object
console.log(typeof new Date); //object
console.log(typeof 5); //number
console.log(typeof 'sdfsdfdsf'); //string
console.log(typeof true); //boolean
console.log(typeof null); //object
console.log(typeof myTypeOf2); //function
console.log(typeof undefined); //undefined
console.log(typeof NaN); //number
// 1、需要注意的是,首先为toString()方法是干嘛的
// toString默认返回一个字符串,该字符串可以表示这个对象,如何表示呢,通过[object type]的方式表示,其中type 表示类型。
// 2、还有一个需要注意的是,为什么用call、或者apply
// 一开始仅认为toString是Object原型上的方法,只能通过Object来调用,call、apply作用仅改变this指向。后来才得知,更是为了防止方法被重写,如下:
let x = {
toString() {
return "jjwu";
}
};
x.toString(); // => "jjwu"
Object.prototype.toString.call(x); // => "[object Object]"
Reflect.apply(Object.prototype.toString, x, []); // => "[object Object]"
</script>
5、数组去重
<script>
// 1、先来看下ES5的去重吧
// fitler加indexOf
function myUnique_1(arr) {
// 首先filter函数是返回满足条件的元素组成一个新的数组,这里就可以看出来为什么很多数组方法的参数里把数组自己带进去了
let res = arr.filter((item, index, array) => {
// indexOf方法可以判断item在数组中第一次出现的位置,当然也可以判断字符或者字符串在一个大字符串中第一次出现的位置
//拿到元素,判断他在数组里第一次出现的位置,是不是和当前位置一样,一样的话返回true,不一样说明重复了,返回false
return array.indexOf(item) === index
})
return res
}
// 循环加indexOf
function myUnique_2(arr) {
let res = [];
arr.forEach(item => {
// 没找到返回-1
if (res.indexOf(item) === -1) {
res.push(item);
}
})
return res
}
// 循环加includes方法和上一个一样,不写了
// 2、ES6 真的很6
function myUnique_3(arr) {
return [...new Set(arr)];
}
// 我们来试一试叭
let tempArr = [1, 1, 2, 3, 4, 4, 5];
console.log(myUnique_1(tempArr))
console.log(myUnique_2(tempArr))
console.log(myUnique_3(tempArr))
// 这里还藏了一个很有意思的去重
console.log(Array.from(new Set(tempArr)))
// Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组
// 类数组对象就是带有length或者size属性等但不是Array就叫类数组,举个下面的例子
const list = document.querySelectorAll('li');
var listArr = Array.from(list);
// 这里可以理解一下map的实际作用,他的返回的数组是可以控制的
const todos = listArr.map(val => val.innerText);
// 当然,这样写总感觉代码还不够美观。那么其实Array.from有第二个参数,第二个参数就类似map方法。
const list2 = document.querySelectorAll('li');
const todos2 = Array.from(list,val => val.innerText);
console.log(todos) // 打印的是所有li标签文本内容组成的数组
// 这个很有意思多看看,去重后加一
let arr = [12, 45, 97, 9797, 564, 134, 45642]
console.log(Array.from(new Set(arr), item => item + 1)) // [ 13, 46, 98, 9798, 565, 135, 45643 ]
// Array.from还可以将字符串转为数组,这个才有意思!!!!!!!!!!!!!!
let str = 'hello world!';
console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]
</script>