概念
什么是对象: 对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
| let obj = { |
| name: "caixin", |
| age: 35 |
| } |
创建对象的方式
new Object()
| let obj = new Object(); |
| |
| obj.name = "caixin" |
字面量方式
| let obj = { |
| name: "caixin", |
| age: 35 |
| } |
使用 Object.create(原型对象,自定义属性对象)
| Object.create(原型对象,自定义属性对象)方法创建一个新对象,使用现有的对象来提供新创建的对象的 _ proto _ |
| |
| let tempObj = { |
| x: 1, |
| y: 2 |
| } |
| |
| let newAttrObj = { |
| a:{ |
| writable:true, |
| configurable:true, |
| value:100 |
| } |
| } |
| |
| let obj = Object.create(tempObj,newAttrObj); |
| |
| console.log(obj) |
| |
| => { |
| a:100, |
| _proto_:{ |
| x:1, |
| y:2 |
| } |
| } |
| |
| 创建一个普通对象 |
| |
| let obj_1 = Object.create(Object.prototype) |
| obj_1.toString(); => [object,object] |
| |
| 创建一个空对象,但该对象不继承任何属性和方法 |
| |
| let obj_2 = Object.create(null) |
| obj_2.toString() => TypeError: obj2.toString is not a function |
工厂方式
| function createPerson(name,age){ |
| |
| let person = new Object(); |
| |
| person.name = name; |
| person.age = age; |
| |
| return person; |
| } |
| |
| let person_1 = createPerson("caixin",35) |
构造函数
| function Person(name,age){ |
| this.name = name; |
| this.age = age; |
| this.sayHi = function (){ |
| console.log("hello") |
| } |
| } |
| |
| let person_1 = new Person("caixin",35) |
| |
| 扩展: 实例对象是通过构造函数来创建的,创建的过程叫实例化。 |
_ proto _
| 实例对象 person_1 有个属性 _ proto _ ,称之为原型,这既是一个属性也是一个对象,这个属性是给浏览器使用的,不是标准的属性,它的下面还有一个属性 constructor ,称之为构造器,constructor 的值指向生成该对象实例的构造函数 Person 。 |
| |
prototype
| 构造函数 Person 下面有一个属性 prototype,也称之为原型,这个属性是给程序员使用的,是标准属性,它的下面也有一个属性 constructor ,也叫构造器,constructor 的值指向自己 。 |
| |
_ proto _ 与 prototype 的关系
| js对象都有 _ proto _ 属性,也就是构造函数 Person 与实例对象 person_1 都有 |
| |
| 只有函数/方法才有 prototype 属性 |
| |
| 实例对象的 _ proto _ 指向了构造函数的原型对象 prototype |
| |
| 实例对象的构造器指向创建自己的构造函数 |
| |
| console.log(person_1._proto_.constructor == Person) => true |
| |
| console.log(person_1._proto_.constructor == Person.prototype.constructor) => true |
如何判断实例对象是不是这种数据类型
| console.log(d instanceof Person) => false |
| |
| 说明对象实例d不是由构造函数Person来创建的 |
对象原型实现数据共享
| 构造函数的原型对象( prototype )中的方法是可以被实例对象直接访问的 |
| |
| 通过原型来添加方法,解决数据共享,节省内存空间。 |
| |
| 原型中的方法是可以互相访问的 |
new 操作符具体干了什么 ?
| 1、在内存中创建一个空的对象 |
| |
| 2、让 this 指向这个空的对象,并继承改对象原型 |
| |
| 3、调用构造函数,目的是给对象属性和方法 |
| |
| 4、返回一个对象 |
键名 ( 属性 )
| 对象的所有键名都是字符串,ES6 引入了 Symbol 值也可以作为键名,所以加不加引号都可以 |
| |
| 如果键名是数值,会被自动转为字符串 |
| |
| 如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错 |
| |
| let obj = {1p:'caixin'} => 报错 |
| |
| 对象的每一个键名又称之为‘属性’,它的‘键值’可以是任何类型。包括 function。如果属性的值还是一个对象,就行成了链式引用。 |
| |
| 属性可以动态创建,不必在对象声明时就指定。 |
对象的引用
| 如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。 |
| |
| let obj1 = {} |
| let obj2 = {} |
| |
| obj1.a = 1 |
| obj2.a => 1 |
| |
| obj2.b = 2 |
| obj1.b => 2 |
| |
| 注意: 如果取消某一个变量对于原对象的引用,不会影响另一个变量 |
| |
| let obj1 = {} |
| let obj2 = o1 |
| |
| obj1 = 1 |
| obj2 => {} |
| |
| 注意: 如果两个变量指向同一个原始类型的值,那么,变量这时都是值的拷贝 |
| |
| let x = 1 |
| let y = x |
| |
| x = 2 |
| |
| y => 1 |
表达式还是语句
对象采用大括号表示,这导致一个问题: 如果行首是一个大括号,它到底是表达式还是语句 ?
| 为了避免这个歧义,javascript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。 |
| |
| { console.log(1) } => 1 |
| |
| ( {foo:123} ) => 123 |
| |
| ( console.log(123) ) |
| |
| 如果要解释为对象,最好在大括号前加上圆括号,因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。 |
对象属性的操作
属性的读取
| 读取对象有两种方法: 点运算符 和 方括号运算符 |
| |
| let obj = { |
| name:'caixin' |
| } |
| |
| obj.name => caixin |
| obj['name'] => caixin |
| |
| 注意: |
| |
| 1、如果是方括号运算符,键名必须是在引号里面,否则会当作变量处理 |
| |
| let foo = "bar" |
| |
| let obj = { |
| foo:1, |
| bar:2, |
| } |
| |
| obj.foo => 1 |
| obj[foo] => 2 没有引号,当作变量处理 |
| |
| 2、方括号内可以使用表达式 |
| |
| obj['f' + 'oo'] => 1 |
| |
| 3、数值键可以不加引号,但会自动转成字符串;数值键不能使用点运算符,会被当成小数点,只能使用方括号。 |
属性的查看
| Object.keys() 和 for in 语句 |
| |
| let obj = { |
| name:"caixin" |
| age:"35" |
| } |
| |
| console.log(Object.keys(obj)) => ["name","age"] |
| |
| for (let i in obj){ |
| console.log(i) => name age |
| } |
| |
| 注意 不能使用 for of 语句 |
| |
| for(let i of obj){ |
| console.log('for of',i) => obj is not iterable |
| } |
属性的删除
| delete 命令用于删除对象的属性,删除成功后返回 true |
| |
| let obj = { p: 1 } |
| |
| Object.keys(obj) => ["p"] |
| |
| delete obj.p => true |
| |
| obj.p => undefined |
| |
| Object.keys(obj) => [] |
| |
| 注意: |
| |
| 1、删除一个不存在的属性,delete 不报错,而且返回 true , 因此不能根据 delete 命令的结果认定某个属性是否存在。 |
| |
| 2、只有一种情况 delete 命令会返回 false , 那就是该属性存在,且不得删除。delete 只能删除对象本身的属性,无法删除继承的属性。 |
| |
| 3、删除数组元素,数组的长度不受影响 |
| |
| let arr = [1,2,3] |
| |
| delete arr[1]; |
| console.log(arr) => [1,empty,3] |
属性的检测
| 1、in 运算符用于检查某个对象是否包含某个属性,如果包含返回 true 否则返回 false ( 可检测自有属性和继承属性,如果包含则返回 true) |
| |
| let obj = { name:'caixn' } |
| |
| console.log( 'name' in obj ) => true |
| console.log( 'toString' in obj ) => true |
| console.log('age' in obj ) => false |
| |
| 注意: |
| |
| 1、in 运算符有一个问题,不能识别哪些属性是自身的,哪些属性是继承的。 |
| |
| 2、可以使用对象的 hasOwnProperty 方法判断一下,是否为对象自身的属性。 |
| |
| let obj = {} |
| |
| if('toString' in obj){ |
| console.log(obj.hasOwnProperty('toString')) => false |
| } |
| |
| 2、hasOwnProperty() |
| |
| 可检测自有属性,包含则返回true。如果是继承属性将返回false。 |
| |
| var o = {x: 1, k: undefined}; |
| |
| o.hasOwnProperty("x"); |
| o.hasOwnProperty("k"); |
| o.hasOwnProperty("y"); |
| o.hasOwnProperty("toString"); |
| |
| 3、propertyIsEnumerable() |
| |
| hasOwnpreperty()的增强版, 只有检测到是自有属性且该属性的可枚举性为true时,才返回true。 |
| |
| 4、使用 !== |
| |
| 通过 !== 判断一个属性是否是 undefined,如果属性值为 undefined 则无法检测 |
对象属性的类型
对象属性是由名字、值和一组特性(attribute)组成。在 es5 中 ,属性值可以用一个或者两个方法替代,就是 getter 和 setter 。由 getter 和 setter 定义的属性称为 “存储器属性” 也叫做 访问器属性。
对象的每个属性都有一个描述对象 (Descriptor),用来控制属性的行为。
ECMAScript 中有两种属性: 数据属性 和 访问器属性
数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有 4 个 描述其行为的特性,又叫属性特性。
属性名 |
说明 |
默认值 |
configurable |
能否通过 delete 删除属性从而重新定义属性;能否修改属性的特性;能否把属性修改为访问器属性 |
true |
enumerable |
能否通过 for - in 循环返回属性 |
true |
writable |
能否修改属性的值 |
true |
value |
属性的数据值,属性值存储和读取都是这个位置 |
undefined |
可通过 Object.getOwnPropertyDescriptor() 查看属性的描述对象
| 使用: Object.getOwnPropertyDescriptor(属性所在对象,属性名) |
| |
| let obj = { name: "caixin" }; |
| |
| let attributeObj = Object.getOwnPropertyDescriptor(obj,name) |
| |
| console.log(attributeObj) |
| |
| => { |
| configurable:true, |
| enumerable:true, |
| writable:true, |
| value:"caixin" |
| } |
可通过 Object.defineProperty() 修改属性默认特性
| 使用: Object.defineProperty(属性所在对象,属性名,描述符对象) |
| |
| let obj = {}; |
| |
| Object.defineProperty(obj,"name",{ |
| configurable: false, |
| enumerable: false, |
| writable: false, => 不可修改 |
| value: "caixin" |
| }) |
| |
| console.log(obj.name) => "caixin" |
| |
| obj.name = "caiqiran" |
| |
| console.log(obj.name) => "caixin" 修改无效 |
| |
| 注意: |
| |
| 1、使用 Object.definedProperty() 定义新的属性,描述对象的属性默认值为false |
| |
| let obj = { age: 35} |
| |
| Object.definedProperty(obj,"name",{ |
| value: "caixin" |
| }) |
| |
| console.log(Object.getOwnPropertyDescriptor(obj,"name")) |
| |
| 新的属性,描述对象属性默认是 false |
| |
| => { |
| configurable: false |
| enumerable: false |
| writable: false |
| value:"caixin" |
| } |
| |
| Object.definedProperty(obj,"age",{ |
| value: 32 |
| configurable: false |
| }) |
| |
| console.log(Object.getOwnPropertyDescriptor(obj,"age")) |
| |
| 如果是已有属性则只影响设置的属性 |
| |
| => { |
| configurable: false |
| enumerable: true |
| writable: true |
| value:32 |
| } |
| |
| 2、一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用 Object.defineProperty()方法修改除 writable 之外的特性,都会导致错误。 |
| |
| var person = {}; |
| |
| Object.defineProperty(person, "name", { |
| configurable: false, |
| value: "Nicholas" |
| }); |
| |
| => 抛出错误 |
| Object.defineProperty(person, "name", { |
| configurable: true, |
| value: "Nicholas" |
| }); |
访问器属性
访问器属性不包含数据值;它们包含一对 getter 和 setter 函数(不过,这两个函数都不是必需的)。
读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值。
写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。
属性名 |
说明 |
默认值 |
get |
读取属性时调用的函数,只有 get 则是一个只读属性 |
undefined |
set |
写入属性时调用的函数,只有 set 则是一个只写属性 |
undefined |
注意:访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义。
使用 Object.defineProperty() 定义单个属性
| let book = { |
| _year: 2022, |
| edition: 3 |
| } |
| |
| Object.defineProperty(book,"year",{ |
| get:function(){ |
| return this._year; |
| }, |
| |
| set:function(newValue){ |
| if(newValue > 2005){ |
| this._year = newValue; |
| this.edition += newValue - 2018; |
| } |
| } |
| }) |
| |
| book.year = 2020; |
| |
| console.log(book.year) => 2020 |
| console.log(book.edition) => 3 |
使用Object.defineProperties()定义多个属性
| let book = {}; |
| |
| Object.defineProperties(book,{ |
| _year:{ |
| value:2022 => 数据属性 |
| }, |
| edition:{ |
| value:1 => 数据属性 |
| }, |
| year:{ => 访问器属性 |
| get:function(){ |
| return this._year; |
| }, |
| set:function(newValue){ |
| if (newValue > 2018) { |
| this._year = newValue; |
| this.edition += newValue - 2018; |
| } |
| } |
| } |
| }) |
对象属性类型总结
1、数据属性是属性自带的,数据属性可以被继承。
2、访问器属性是需要创建的,访问器属性可以被继承。
3、相关方法
方法 |
作用 |
说明 |
Object.getOwnPropertyDescriptor() |
属性的描述对象 |
- |
Object.defineProperty() |
修改属性默认特性/定义单个属性 |
数据属性和访问器属性都可用,使用后数据属性默认值为 false |
Object.defineProperties() |
定义多个属性 |
数据属性和访问器属性可用,但注意数据属性设置 false 后不能修改为 true,所以慎用 |
对象的固有属性
每个对象都有: 原型属性(prototype)、类 (class)、可拓展性(extensible)这 三个 属性
原型属性 prototype
| 对象的 原型属性 是用来 继承属性 的,原型属性 也简称为 原型。 |
原型链
| 谈到继承时,javaScript 只有一种结构: 对象。 |
| |
| 每个实例对象 Object 都有一个私有属性 (称之为 _proto_ )指向它的构造函数的原型对象 protoptype 。该原型对象也有一个自己的原型对象 ( proto ),层层向上直到一个对象的原型对象为 null 。这种链式结构叫做: 原型链。 |
| |
| null 没有原型,并作为这个原型链中的最后一个环节。也可表示空对象指针。 |
__ proto __ 和 prototype 的关系和区别
| 在所有实现中,对象实例都没办法通过 [[prototype]] 来访问原型,所以定义了 __proto__ 指向对象构造函数的 prototype 。[[prototype]] 可以使用 __proto__ 访问。 |
| |
| 1、prototype 是对象的原型属性,可被继承 |
| |
| 2、__proto__ 指向对象构造函数的 prototype |
| |
| 3、constructor 属性将原型对象指向关联的构造函数 |
isPrototypeOf()
| isPrototypeOf(): 测试一个对象是否存在于另一个对象的原型链上 |
| |
| function Foo() {} |
| function Bar() {} |
| function Baz() {} |
| |
| Bar.prototype = Object.create(Foo.prototype); |
| Baz.prototype = Object.create(Bar.prototype); |
| |
| var baz = new Baz(); |
| |
| console.log(Baz.prototype.isPrototypeOf(baz)); => true |
| console.log(Bar.prototype.isPrototypeOf(baz)); => true |
| console.log(Foo.prototype.isPrototypeOf(baz)); => true |
| console.log(Object.prototype.isPrototypeOf(baz)); => true |
类属性 class
对象的类属性(class attribute)是一个字符串,用来表示对象的类型信息。可以通过 toString() 方法来查询它。 默认的 toString() 方法会返回下面格式的字符串:
[object class]
toString()方法可以被重写,所以为了能得到正确的数据,需要间接调用 Function.call() 方法。
| function classof(o) { |
| return Object.prototype.toString.call(o); |
| |
| } |
| |
| classof(null); => [object Null] |
| classof(undefined); => [object Undefined] |
| classof(1); => [object Number] |
| classof(""); => [object String] |
| classof(false); => [object Boolean] |
| classof({}); => [object Object] |
| classof([]); => [object Array] |
| classof(/./); => [object RegExp] |
| classof(new Date()); => [object Date] |
| classof(window); => [object Window] |
可拓展性
对象的可拓展性表示是否可以给对象添加新属性。所有内置对象和自定义对象都是显示可拓展的。
检测可拓展性
| Object.isExtensible() |
| |
| Object.isSealed() |
| |
| Object.isFrozen() |
影响可拓展性的方法
| Object.preventExtensions() |
| |
| Object.seal() |
| |
| Object.freeze() |
注意
| 1、一旦转为不可拓展后,就无法再转回可拓展 |
| |
| 2、preventExtensions 只影响到对象本身的可拓展性 |
| |
| var a = {}; |
| |
| Object.isExtensible(a); => true |
| Object.preventExtensions(a); => Object.seal()、Object.freeze() 同理 |
| |
| a.name = 'a'; |
| a; => {} |
| |
| Object.isExtensible(a); => false |
对象的方法
对象原型上 继承 的方法
方法名 |
功能 |
hasOwnProperty() |
检测自身属性 |
propertyIsEnumerable() |
检测自身属性且该属性的可枚举性为 true 时 |
isPrototypeOf() |
测试一个对象是否存在于另一个对象的原型链上 |
toString() |
返回表示调用这个方法的对象值的字符串 |
toLocaleString() |
返回本地化的字符串 |
toJSON() |
返回序列化的结果 |
valueOf() |
将对象转换为原始值 |
| const a = {name: 123}; |
| |
| a.toString(); => [object Object] |
| a.toLocaleString(); => [object Object] |
| a.valueOf(); => {name: 123} |
| |
| const b = [1, 2, 3] |
| |
| b.toString(); => 1,2,3 |
| b.toLocaleString(); |
| b.valueOf(); => [1, 2, 3] |
| |
| const d = new Date(); |
| |
| d.toString(); => Fri Aug 28 2020 11:29:42 GMT+0800 (中国标准时间) |
| d.toLocaleString(); => 2020/8/28 上午11:29:42 |
| d.toJSON(); => 2020-08-28T03:29:42.460Z |
| d.valueof(); => 1598585382460 |
Object 构造函数 上 的方法
Object 属性
1、Object.length: 值为 1
2、Object.prototype: 可以为所有 Object 类型的对象添加属性
ES5 方法
方法名 |
功能 |
Object.create() |
使用指定的原型对象和属性创建一个新对象 |
Object.defineProperty() |
给对象添加一个属性并指定该属性的配置 |
Object.defineProperties() |
给对象添加多个属性并分别指定它们的配置 |
Object.getOwnPropertyNames() |
返回一个数组,它包含了指定对象所有的可枚举或不可枚举的属性 |
Object.getOwnPropertySymbols() |
返回一个数组,它包含了指定对象自身所有的符号属性 |
Object.getOwnPropertyDescriptor() |
返回对象指定的属性配置 |
Object.isExtensible() |
判断对象是否可扩展 |
Object.preventExtensions() |
防止对象的任何扩展 |
Object.seal() |
创建一个“密封”对象,实际上是调用Object.preventExtensions()把所有属性标记为configurable:false,仅可修改属性的值 |
Object.isSealed() |
判断对象是否已经密封 |
Object.freeze() |
创建一个“冻结”对象,实际上调用Object.seal(),并把所有属性标记为writable:false |
Object.isFrozen() |
判断对象是否已经冻结 |
ES6 方法
方法名 |
功能 |
Object.is() |
比较两个值是否相同。所有 NaN 值都相等(这与和=不同) |
Object.assign() |
通过复制一个或多个对象来创建一个新的对象 |
Object.getOwnPropertyDescriptors() |
返回对象所有属性的描述对象 |
Object.setPrototypeOf() |
设置对象的原型(即内部 [[Prototype]] 属性) |
Object.getPrototypeOf() |
返回指定对象的原型对象 |
Object.keys() |
返回一个包含所有给定对象自身可枚举属性名称的数组 |
Object.values() |
返回给定对象自身可枚举值的数组 |
Object.entries() |
返回给定对象自身可枚举属性的 [key, value] 数 |
Object.fromEntries() |
Object.entries()的逆操作,用于将一个键值对数组转为对象 |
javascript对象类别划分
原生对象 ( native object )
原生对象也可以叫做本地对象或者内部对象
ECMA-262 把原生对象(native object)定义为: "独立于宿主环境的 ECMAScript 实现提供的对象"
所以每一种宿主环境都可以使用原生对象
| JavaScript中的原生对象有 |
| |
| Object、Function、Array、String、Boolean、Math、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError和Global |
内置对象 ( built-in object )
ECMA-262 把内置对象(built-in object)定义为: "由 ECMAScript 实现提供的、独立于宿主环境的所有对象,在 ECMAScript 程序开始执行时出现"
前半部分和原生对象很像,实际上内置对象也都是原生对象,区别在于后半句 "在 ECMAScript程序开始执行时出现"
这意味着开发者无需使用 new + 构造函数 创建,而是JavaScript引擎初始化的时候就被创建,可以直接使用
| 目前定义的内置对象只有两个 |
| |
| Global 和 Math |
| |
| 内置对象是原生对象的一种,主要区别在于是否需要实例化 |
| |
| 在 ECMAScript 中,不存在独立的函数,所有函数都必须是某个对象的方法 |
| |
| 在使用原生对象时,我们一般都需要var obj = new Object() 或字面量 var obj = {} 明确实例化生成一个实例再去调用 Object 的某个方法 obj.toSting() |
| |
| 但如果是内置对象 Global 和 Math,我们只需Math.floor(2.4) 直接调用,不需要再进行实例化 |
宿主对象 ( host object )
什么是宿主,ECMA 仅是一套规范,也就是指定的一套编程规则
规则毕竟是规则,如果要发挥作用,必须要有平台或者说环境,这就是ECMA的宿主
ECMAScript 中的 "宿主" 当然就是我们网页的运行环境,即 "操作系统" 和 "浏览器"
所有非原生对象都是宿主对象(host object),即由 ECMAScript 实现的宿主环境提供的对象
| 所有的 BOM 和 DOM 对象都是宿主对象,它们都属于window对象的子对象 |
| |
| 不同的宿主环境所展示的内容不同 |
| |
| 它不是 ECMAScript 官方提供的,而是浏览器这个宿主为了方便开发者而加上去的 |
| |
| 实际上所有非原生对象都是宿主对象 |
深浅拷贝
变量的存储方式 栈(stack)和 堆(heap)
| 栈 |
| |
| 自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址 |
| |
| 堆 |
| |
| 动态分配的内存,大小不定,也不会自动释放。里面存放引用类型的值 |
| |
| |
| 在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。 |
| |
| 基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值 |
| |
| 引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量 |
深拷贝及实现方法
| 基本类型不存在浅拷贝还是深拷贝的问题,主要是针对于引用类型 |
| |
| 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据 |
| |
| 也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的 |
| |
| 深拷贝的实现方式 |
| |
| 1、JSON.parse(JSON.stringify()) |
| |
| 利用JSON.stringify将对象转成JSON字符串 |
| |
| 再用JSON.parse把字符串解析成对象 |
| |
| 一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝 |
| |
| 缺点 |
| |
| 不能处理 函数 和 正则 undefined symbol |
| |
| JSON.stringify 和 JSON.parse 处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了 |
| |
| 2、递归实现 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构