对象、类与面向对象编程

ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未 讨论的原因),可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组键/值对,值可以是数据或者函数

理解对象

以前创建对象的方法通常是给Object创建一个新的实例,在给他添加属性和方法

javascript 复制代码
let obj = new Object()
obj.name = '张三'
obj.age = 18
obj.say = ()=>{
  consloe.log('hello world')
}

上面的代码我们给obj对象添加了属性和方法,现在我们一般使用对象字面量的方式创建对象

javascript 复制代码
let obj = {
  name:'张三',
  age:18,
  say(){
    consloe.log('hello world')
  }
}

这种方式创建的obj和上面的obj是一样, 它们的属性和方法都一样。

属性的类型

属性分两种:数据属性访问器属性

  1. 数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。

  • \[Configurable\]\]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true

  • \[Writable\]\]: 表示属性的值是否可以被修改 。默认为true

如同上面代码一样将属性显示的添加到对象,这里的4个属性都会被设置为默认值。如果我们想修改属性的默认值,可以通过Object.defineProperty()方法。 这个方法接收 3 个参数: 要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包 含:configurable、enumerable、writable 和 value,跟相关特性的名称一一对应。

注意:严格模式下才会抛出错误,非严格模式调用时无效果

javascript 复制代码
Object.defineProperty(person,'name',{
  configurable:false,
  enumerable:false,
  writable:false,
  value:'张三'
})
//delete person.name // Cannot delete property 'name' of #<Object>
//person.name = '法外狂徒' //Cannot assign to read only property 'name' of object '#<Object>'
for (const key in person) {
  console.log('key',key);// 未执行
}

注意:在使用Object.defineProperty()的时候,如果没有指定 ConfigurableEnumerable writable的值,那么都默认为false

访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有 4 个特性描述它们的行为。

  • \[Configurable\]\]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true

  • \[Get\]\]: 获取函数,在读取属性时调用。默认值为 undefined。

javascript 复制代码
let book = {
  year_: 2017,
  edition: 1
}
Object.defineProperty(book, 'year', {
  get() {
    return this.year_
  },
  set(newValue) {
    if (newValue > 2017) {
      this.year_ = newValue
      this.edition += newValue - 2017
    }
  }
})
book.year = 2018
console.log(book.edition) //2

这里的year被定义为一个访问器属性,这是访问器属性的典型使用场景,即设置一个属性 值会导致一些其他变化发生;获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。\

定义多个属性

在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应

javascript 复制代码
let book = {}
Object.defineProperties(book, {
  year_: {
    value: 2017
  },
  edition: {
    value: 1
  },
  year: {
    get() {
      return this.year_
    },
    set(newValue) {
      if (newValue > 2017) {
        this.year_ = newValue
        this.edition += newValue - 2017
      }
    }
  }
})

在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。 最终的对象跟上一个例子中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的 configurable、enumerable 和 writable 特性值都是 false

读取属性的特性

使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、 writable 和 value 属性。

javascript 复制代码
let book = {}
Object.defineProperties(book, {
  year_: {
    value: 2017
  },
  edition: {
    value: 1
  },
  year: {
    get: function () {
      return this.year_
    },
    set: function (newValue) {
      if (newValue > 2017) {
        this.year_ = newValue
        this.edition += newValue - 2017
      }
    }
  }
})
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function" 

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()静态方法。这个方法实际上 会在每个自有属性上调用 Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。

javascript 复制代码
let book = {}
Object.defineProperties(book, {
  year_: {
    value: 2017
  },
  edition: {
    value: 1
  },
  year: {
    get: function () {
      return this.year_
    },
    set: function (newValue) {
      if (newValue > 2017) {
        this.year_ = newValue
        this.edition += newValue - 2017
      }
    }
  }
})
console.log(Object.getOwnPropertyDescriptors(book))
// {
// 	edition: {
// 		configurable: false,
// 		enumerable: false,
// 		value: 1,
// 		writable: false
// 	},
// 	year: {
// 		configurable: false,
// 		enumerable: false,
// 		get: f(),
// 		set: f(newValue),
// 	},
// 	year_: {
// 		configurable: false,
// 		enumerable: false,
// 		value: 2017,
// 		writable: false
// 	}
// }

对象合并

ECMAScript 6 专门为合并对象提供了 Object.assign()方法。此方法接受一个目标对象和一个或多个源对象参数,将源对象上的属性复制到目标对象

javascript 复制代码
let obj = {name:'张三'}
let obj1 = {}
let result = Object.assign(obj1,obj)
console.log(result) // {name:'张三'}
console.log(obj1) // {name:'张三'}
console.log(obj1===result) //true

Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数

set() 方法并没有合并到obj1对象上。

当有相同属性时,使用后一个复制的值

javascript 复制代码
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true 

Object.assign()实际上对每个源对象执行的是浅复制

增强的对象语法

  1. 属性简写
javascript 复制代码
let name = 'Matt';
let person = {
 name: name
};

//简写为 
let person = {
  name
}
  1. 可计算属性

在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性

javascript 复制代码
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }

有了可计算属性,就可以在对象字面量中完成动态属性赋值

javascript 复制代码
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
 [nameKey]: 'Matt',
 [ageKey]: 27,
 [jobKey]: 'Software engineer'
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }

因为被当作 JavaScript 表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值

javascript 复制代码
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
 return `${key}_${uniqueToken++}`;
}
let person = {
 [getUniqueKey(nameKey)]: 'Matt',
 [getUniqueKey(ageKey)]: 27,
 [getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
  1. 简写方法名
javascript 复制代码
let person = {
 sayName: function(name) {
 console.log(`My name is ${name}`);
 }
};
person.sayName('Matt'); // My name is Matt
//简写
let person = {
 sayName(name) {
 console.log(`My name is ${name}`);
 }
};
person.sayName('Matt'); // My name is Matt

简写方法名与可计算属性键相互兼容

javascript 复制代码
const methodKey = 'sayName';
let person = {
 [methodKey](name) {
 console.log(`My name is ${name}`);
 }
}
person.sayName('Matt'); // My name is Matt

对象解构

ECMAScript 6 新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简 单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。 解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则 该变量的值就是 undefined:

javascript 复制代码
// 不使用解构
let obj = {name:'张三',age:18,gender:'男'}
let name = obj.name
let age = obj.age
let gender = obj.gender
console.log(name,age,gender) // 张三  18  男

// 使用解构
let obj = {name:'张三',age:18,gender:'男'}
const {name,age,gender} = obj
console.log(name,age,gender) // 张三  18  男
// 解构重命名
const {name:n,age:a,gender:g} = obj
console.log(n,a,g) // 张三  18  男
// 引用的属性不存在
const {address} = obj
console.log(address) // undefined
// 定义默认值
const {address='龙华'} = obj
console.log(address) // 龙华
// 是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中
let name, age;
({name,age}) = obj

null和 undefined 不能被解构

  1. 嵌套解构

解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性

javascript 复制代码
let person = {
 name: '张三',
 age: 27,
 job: {
 title: '法外狂徒'
 }
};
let personCopy = {}
({name:personCopy.name,age:personCopy.age,} = person)
// 因为一个对象的引用被赋值给 personCopy,所以修改person.job 对象的属性也会影响 personCopy
person.job.title = '罗老师'
console.log(person) //{name: '张三',age: 27,job: {title: '罗老师'}}
console.log(personCopy) //{name: '张三',age: 27,job: {title: '罗老师'}}
// 解构赋值可以使用嵌套结构,以匹配嵌套的属性
const {obj:{title}} = person
console.log(title) //罗老师
// 在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样
let person = {
 job: {
 title: 'Software engineer'
 }
};
let personCopy = {};
// foo 在源对象上是 undefined
({
 foo: {
 bar: personCopy.bar
 }
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job 在目标对象上是 undefined
({
 job: {
 title: personCopy.job.title
 }
} = person);
// TypeError: Cannot set property 'title' of undefined
  1. 部分解构

如果一个解构表达式涉及 多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分

javascript 复制代码
let person = {
 name: 'Matt',
 age: 27
};
let personName, personBar, personAge;
try {
 // person.foo 是 undefined,因此会抛出错误
 ({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {}
console.log(personName, personBar, personAge);
// Matt, undefined, undefined 
  1. 参数上下文匹配

在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在函数签名中声明在函数体内使用局部变量

javascript 复制代码
let person = {
 name: 'Matt',
 age: 27
};
function printPerson(foo, {name, age}, bar) {
 console.log(arguments); // 123  { name: 'Matt', age: 27 } abc
 console.log(name, age); //Matt 27
} 
printPerson('123',person,'abc')

创建对象

虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。我们可以使用一种称为面向对象编程的编程范式来解决这个问题。

工厂模式

工厂模式是一种创建型设计模式,通过工厂函数来封装对象的创建过程,避免在代码中直接使用 new关键字创建对象。工厂模式简化了对象的创建过程,降低了代码的耦合性,提高了代码的可维护性和可读性。工厂模式可以返回不同类型的对象,具体的实现细节可以隐藏在工厂函数的内部,使得代码更加模块化。另外,通过工厂模式,我们可以在代码中使用抽象的接口而不是具体的对象类型,从而在代码重构时变得更加灵活

javascript 复制代码
function createPerson(name, age, job) {
 let o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function() {
 console.log(this.name);
 };
 return o;
}
let person1 = createPerson("张三", 29, "法外狂徒");
let person2 = createPerson("孙悟空", 2700, "弼马温")

函数 createPerson()接收 3 个参数,根据这几个参数构建了一个包含 Person 信息的对象。 可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象
工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(不能保证对象之间的唯一性)

构造函数模式

构造函数是用于创建特定类型对象的。ObjectArray是原生的构造函数, 运行时可以直接在执行环境中使用。我们也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

javascript 复制代码
function Person(name, age, job){
	this.name = name
  this.age = age
  this.job = job
  this.say=function(){
    console.log('hellow,world')
  }
}
let person1 = new Person("张三", 29, "法外狂徒")
let person2 = new Person("孙悟空", 2700, "弼马温")
person1.say(); // 张三
person2.say(); // 孙悟空

console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true 

在这个例子中,Person函数取代了工厂函数中的createPerson函数,实际上Person函数内部createPerson函数基本是相同的,只有以下几点不同

  1. 没有显式地创建对象。
  2. 属性和方法直接赋值给了 this。
  3. 没有 return。

constructor是用于标识对象类型的,不过我们一般使用 instanceof操作符来确定对象类型。

定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。
注意:构造函数首字母大写

要创建 Person 的实例,应使用 new 操作符。 以这种方式调用构造函数会执行如下操作

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
javascript 复制代码
// 手写new
function new(fun,...args){
  // 创建一个新对象,并将其 __proto__ 属性设置为构造函数的原型对象
  ler newObj = Object.create(fun.prototype)
  // 将构造函数的上下文绑定到新创建的对象上,并执行构造函数
  const result = fun.apply(fun,newObj)
	// 返回新创建的对象,如果构造函数显式返回了一个对象,则返回该对象,否则返回新对象
  return result instanceof Object ? result : obj; 
}
  • 构造函数也是函数

构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个 函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。像上面的Person()函数可以调用就是一个普通函数,使用new 调用就是构造函数

javascript 复制代码
// 普通函数
Person("张三", 29, "法外狂徒")
window.say() // 张三
// 构造函数
let person = new Person("张三", 29, "法外狂徒")
person.say() // 张三
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "张三", 29, "法外狂徒");
o.say(); // 张三

在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用 call()/apply()调用),this始终指向Global对象(浏览器里就是window对象),所以普通函数调用时,window对象上有了一个say()方法;在另一个函数的作用域调用的情况下,使用了call()(或 apply())方法,会改变this的指向,这里的this指向了o对象,所以所有属性和 say()方法都会添加到对象 o 上面。

原型模式

每个函数都会创建一个prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。

javascript 复制代码
function Person(){}
Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.say=function(){
  console.log(this.name)
}
let p1 = new Person()
let p2 = new Person()
p1.say() //张三
p2.say() //张三
console.log(p1.say==p2.say) //true

这里的属性和方法都添加到了prototype上,构造函数体中什么都没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 p1 和 p2 访问的都是相同的属性和相同的 say()函数

  1. 理解原型

只要创建一个函数就会为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有的原型对象都有一个constructor指回构造函数,例如 Person.prototype.constructor 指向Proson。

在自定义构造函数时,原型对象默认只会获得constructor属性,其他的方法都继承Object。每次调用构造函数都会创建一个实例,这个实例的内部Prototype指针会指向被赋值为构造函数的原型对象。我们可以通过__proto__属性访问对象的原型。

javascript 复制代码
console.log(p1.__proto__ == Person.prototype) //true
console.log(p1.__proto__.__proto__.constructor==Object) //true
console.log(p1.__proto__.__proto__.__proto__==null) //true

** 正常的原型链都会终止于 Object 的原型对象。 Object 原型的原型是 null**

Person构造函数、Person原型对象、Person实例之间的关系。

Person构造函数: Person.prototype指向原型对象,Person.prototype.contructor指回构造函数。

实例对象 p1.__proto__指回Person.prototype

不是所有的实现都对外暴露了[[Peototype]],所以js给我们提供了isPrototypeOf()方法来测试一个对象是否为另一个对象的原型

javascript 复制代码
console.log(Person.prototype.isPrototypeOf(p1)) // true 
console.log(Object.prototype.isPrototypeOf(Person)) // true  所有 JavaScript 对象都继承自 Object.prototype

在ES5中,我们可以使用Object.getPrototypeOf()来获取原型对象

javascript 复制代码
console.log(Object.getPrototypeOf(p1) == Person.prototype)

在ES6中,我们可以使用Object.setPrototypeOf()来设置原型对象

javascript 复制代码
const Person = {
  name:'',
  age:18,
  say(){
    console.log(`我是${this.name}`)
  }
}
let p = {
  name:'张三'
}
Object.setPrototypeOf(p,Person) //将 Person 设置为 p 的原型对象
console.log(p.say())

不过 Object.setPrototypeOf()方法可能会造成较大的性能影响,我们一般使用Object.create()来创建新对象,并为其指定原型

javascript 复制代码
const Person = {
  name:'',
  age:18,
  say(){
    console.log(`我是${this.name}`)
  }
}
let p = Object.create(Person)
p.name = '张三'
console.log(p.say())
  1. 原型层级

在通过对象访问属性时,会按照这个属性的名称开始搜索,先搜索对象实例本身,如果有就返回,没有就去原型对象上找,如果有就返回,没有就一直找到Object,还找不到就返回null。

如果在原型对象和实例对象上都有相同的属性,实例对象的属性会遮住原型对象的属性,优先返回实例对象的值

可以使用hasOwnProperty()方法确定属性是在实例上还是原型对象上。属性存在于调用它的对象实例上时返回true。

javascript 复制代码
function Person(){}
Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.say = function(){
  console.log(this.name)
}
let p1 = new Person()
console.log(p1.name) //张三  来自原型对象 
console.log(p1.hasOwnProperty('name')) //false 
console.log(Person.hasOwnProperty('name')) //true 
p1.name = '法外狂徒'
console.log(p1.name) // 法外狂徒 来自实例对象
console.log(p1.hasOwnProperty('name')) //true 
console.log(Person.hasOwnProperty('name')) //true 
  1. 原型和in操作符

有两种方式可以使用in操作符:单独使用和在for-in循坏中使用。

3.1 单独使用
in操作符会在可以通过对象访问指定属性时返回true,不管是在实例上还是原型上

javascript 复制代码
function Person(){}
Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.say = function(){
  console.log(this.name)
}
let p1 = new Person()
console.log('name' in p1) //true
console.log('name' in Person) //true

判断一个值是否存在原型上,可以通过inhasOwnproperty()来判断

javascript 复制代码
function hasPrototypeProperty(object, name){
 return !object.hasOwnProperty(name) && (name in object);
}

只要可以通过对象方法,in操作符就返回true,而hasOwnproperty()只有属性存在实例上时才返回true。所以当in返回true且hasOwnproperty()返回false时说明该属性是一个原型属性

3.2 for-in循环使用

for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。

要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法。这个方法接收一个对象作 为参数,返回包含该对象所有可枚举属性名称的字符串数组。

javascript 复制代码
function Person() {}
Person.prototype.name = "张三";
Person.prototype.age = 29;
Person.prototype.job = "法外狂徒";
Person.prototype.sayName = function() {
 console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "[name,age,job,sayName]"
let p1 = new Person();
p1.name = "李四";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"

如果想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames()

javascript 复制代码
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"

constructor是一个不可枚举的属性。

  1. 属性枚举顺序

for-in 循环和 Object.keys() 的枚举顺序是不确定的 ,取决于 JavaScript 引擎,可能因浏览器而异 。

Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign() 的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键

继承

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。 前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签 名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

原型链

原型链继承

javascript 复制代码
function Parent() {
   this.name = 'Parent';
}

Parent.prototype.sayHello = function() {
   console.log('Hello, ' + this.name);
};

function Child() {
   this.name = 'Child';
}

Child.prototype = new Parent(); // 将父对象的实例指定为子对象的原型

var child = new Child();
child.sayHello(); // 输出 'Hello, Child'
相关推荐
辻戋1 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun2 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp2 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.3 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl5 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理8 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻8 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js