js中的原型
构造函数:
通过new一个函数来创建一个实例,那这个函数就是构造函数,箭头函数不能作为构造函数
prototype属性:
是函数特有的属性,让一个构造函数实例化的所有对象都有公共的方法和属性
_proto_属性:
是对象特有的属性,指向当前对象的原型对象
对象的_proto_属性就是他的构造函数的prototype属性
实现继承的几种方式
- 原型链继承
function Parent() {
this.name = "parent";
}
// 在父类的原型上定义方法
Parent.prototype.getName = function() {
return this.name;
}
function Child() {
this.name = "child";
}
// 子类继承父类,这里是关键,实现原型链继承
Child.prototype = new Parent();
// 实例化子类
var child1 = new Child();
console.log(child1.getName()); // 输出 "child"
缺点是包含引用类型值的原型属性会被所有实例共享。 换而言之,如果一个实例改变了该属性,那么其他实例的该属性也会被改变
引用类型值是指 JavaScript 中的对象、数组、函数等。它们的值存储在堆内存中,变量只是保存了指向堆内存的地址(引用)
- 构造函数继承
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类
function Child() {
Parent.call(this);
}
// 创建两个 Child 实例
var child1 = new Child();
var child2 = new Child();
console.log(child1.sayHello === child2.sayHello); // 输出 false
采用复制父类构造函数上的属性和方法的方式来继承,但不能继承父类prototype上的属性和方法。
call()
用于调用一个函数并显示指定函数内部的this值
常见应用场景包括:借用方法、构造函数继承、改变回调函数的 this
等
- 组合继承
// 父类
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类
function Child() {
Parent.call(this);
}
Child.prototype = new Parent();
var child1 = new Child();
先call()再让子类的prototype继承父类prototype上的属性和方法
缺点是调用了两次Parent()
- 寄生组合继承
在组合继承的基础上,把Child.prototype = new Parent()
替换为Child.prototype = Object.create(Parent.prototype)
Object.create(Parent.prototype)
创造了一个空对象,这个空对象的__proto__
是Parent.prototype
。所以它继承了Parent
原型链上的属性和方法。由于我们删除了Child.prototype = new Parent()
我们不再调用Parent()
构造函数,因此Child.prototype
不再包含Parent
的属性和方法。
缺点是Child()
原有的prototype
上的属性和方法会丢失
箭头函数、对象的方法、类的静态方法都没有prototype属性
const obj = {
method: function() {}
};
console.log(obj.method.prototype); // undefined
class Example {
staticMethod() {} //
}
console.log(Example.prototype.staticMethod.prototype); // undefined
class Example {
static staticMethod() {}
}
console.log(Example.staticMethod.prototype); // undefined
//手写instanceof
function instanceOf(example,classBasic){
let classBasicPrototype=classBasic.prototype
let examplePrototype=example._proto_
while(true){
if(classBasicPrototype==examplePrototype) return true
if(!examplePrototype) return false
examplePrototype=examplePrototype._proto_
}
}
//手写类型检测,用typeof&Object.prototype.toString.call()实现
function myMethod(example){
if(example==null) return 'null'
if(typeof(example)!=='object'&&typeof(example)!=='function') {
console.log('typeof')
return typeof(example)
}
return Object.prototype.toString.call(example).toLowerCase().slice(8,-1)
}
js中的数据类型
- 基本类型
number string boolean null undefined symbol bigint
- 引用类型
Object Array Function Map Set...
对于一个基本类型,在调用其方法时js会把他包装成一个包装类型,调用完销毁,但是不能添加属性
typeof
对象类型在内存中存储时机器码是000,null全是0,所以typeof会把null判为object
null&&undefined
Number(null)==0
Number(undefined)==NaN
//safeUndefined
let safeUndefined = void 0
isNaN
Number.isNaN()//不做类型转换,只有NaN返回true
isNaN()//做类型转换,转换不成NaN的话,返回true
数组索引
数字、对象、函数作为数组索引时会先被转换成字符串,Symbol不会
symbol
- 默认的symbol对象两两不同
let id1=Symbol("我是id1")
let id2=Symbol("我是id1")
console.log(id1==id2) //false
console.log(id1.description) //我是id1
- 创建两个相同的symbol对象
let id1=Symbol.for("我是id1")
let id2=Symbol.for("我是id1")
console.log(id1==id2) //true
console.log(Symbol.keyFor(id1)) //我是id1
- symbol 作用
- 隐藏对象属性
在对象内的symbol属性通过key来遍历时是不可见的
- 隐藏对象属性
let id=Symbol()
class student {
constructor(name){
this.name=name
this[id]=666
}
}
let sb=new student()
console.log(sb.name)
for (const key in sb) {console.log(key)}
遍历对象上的symbol属性
for (const key in Object.getOwnPropertySymbols(sb))
for (const key in Reflect.ownkeys(sb))
浅拷贝&&深拷贝
浅拷贝只拷贝一层
实现浅拷贝
- 对象的浅拷贝
const obj1={name:'lucy',age:18,tickets:{price:100,discount:0.8}}
let obj2=Object.assign({},obj1)
let obj3={...obj1}
let obj4={}
for(const key in obj1){//会遍历到原型链上的属性
if(obj1.hasOwnProperty(key)){
obj4[key]=obj1[key]
}
}
Object.keys(obj1).forEach(()=>{
obj4[key]=obj1[key]
})
- 数组的浅拷贝
arr1.slice()
[].concat(arr1)
实现深拷贝
const deepCopy=JSON.parse(JSON.stringify(obj))
- 不会拷贝函数,JSON序列化会自动忽略函数
- 不会拷贝特殊对象,如
Date(),//输出字符串,regex//输出空对象
- 不会拷贝原型链上的属性
- 忽略undefined和Symbol()
手写深拷贝
//手写深拷贝
function deepClone(source,cloneMap=new Map()){
if(typeof(source)!=="object"||source==null){
return source
}
if(cloneMap.has(source)){
return cloneMap.get(source)
}
let target//解决无法拷贝Date()和Reg
if(Array.isArray(typeof(source))){
target=[]
}else if(source instanceof Date){
target=new Date()
}else if(source instanceof RegExp){
target=new RegExp(source.source,source.flags)
}else {
target={}
}
// const target=Array.isArray(source)?[]:{}
cloneMap.set(source,target)//就是单独解决循环调用自身,这里只存了自己指向自己
for (const key in source){
if (typeof source[key]=="object" && source[key]!==null){
target[key]=deepClone(source[key],cloneMap)
}else {
target[key]=source[key]
}
}
//拷贝symbol
const symbolKeys=Object.getOwnPropertySymbols(source)
for(const symKey of symbolKeys){
target[symkey]=deepClone(source[symKey],cloneMap)//symKey属性可能是引用类型
}
return target
}
-
source.source
:获取正则表达式的主体字符串。 -
source.flags
:获取正则表达式的修饰符(如g
、i
、m
等)。
为什么0.1+0.2大于0.3?
>0.1.toString(2)
'0.0001100110011001100110011001100110011001100110011001101'
>0.2.toString(2)
'0.001100110011001100110011001100110011001100110011001101'
>0.1+0.2
0.30000000000000004
>0.3.toString(2)
'0.010011001100110011001100110011001100110011001100110011'
>0.1+0.3
0.4
如何避免浮点数误差?
- 使用
toFixed()
或toPrecision()
进行舍入console.log((0.1 + 0.2).toFixed(1) == "0.3"); // true
- 用
Math.round()
处理精度console.log(Math.round((0.1 + 0.2) * 10) / 10 === 0.3); // true
- 使用大整数计算(乘 10 或 100 再除回去)
console.log((10 * 0.1 + 10 * 0.2) / 10 === 0.3); // true
- 使用
BigDecimal
库(如decimal.js
),适用于需要高精度计算的场景。
parseInt
parseInt(string,radix)
返回一个整数
传入的第一个参数会被String()
转换成字符串
String({})
'[object Object]'
类型转换
1. 加法
Object对象会先调用 ValueOf()
,如果返回值不是基本类型,再调用toString
2. 减法||乘法
都转化成数字