类与对象篇(1)

笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。

第一章: 谈谈你对原型链的理解

原型对象和构造函数有何关系?

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会自带一个prototype属性(显式原型),这个属性指向函数的原型对象 当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个**__proto__属性(隐式原型)**,指向构造函数的原型对象。

javascript 复制代码
// 实例.__proto__ === 原型
// 原型.constructor === 构造函数
// 构造函数.prototype === 原型

// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线。例如: 
const o = new Object()
console.log(o) //{}

// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
console.log(o.hasOwnProperty('constructor'))  //false 
console.log(o.constructor)                       //[Function: Object]
console.log(o.constructor === Object)            // --> true
console.log(o.__proto__.constructor === Object)  // --> true
console.log(o.__proto__.constructor.prototype.constructor === Object)  // --> true

o.__proto__ = null
console.log(o)                         //[Object: null prototype] {}
console.log(o.constructor === Object)  // --> false

能不能描述一下原型链?

JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链

javascript 复制代码
实例对象.__proto__ = 构造函数的.prototype
javascript 复制代码
class A{}
class B extends A{}
class C extends B{}
console.log(new C())

对象的hasOwnProperty() 和 in 的区别?

  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性。执行直接对象查找时,它始终不会查找原型。
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
javascript 复制代码
var obj = {name:'一缕清风'}

'toString' in obj              // true
'hasOwnProperty' in obj        // true
obj.hasOwnProperty('toString') // false

⚠️ **注意: **虽然 in 能检测到原型链的属性,但 for in 通常却不行。

Function 原型链

javascript 复制代码
console.log(Function.prototype);                      // [Function]
console.log(Object.getPrototypeOf(Function));         // [Function]
 
console.log(Function.__proto__);                      // [Function]
console.log(Function.__proto__.__proto__);            // {}
console.log(Function.__proto__.__proto__.__proto__);  //null

第二章: JS如何实现继承?

第一种: 借助call,实现构造函数继承

javascript 复制代码
<script>
  function Parent(age) {
    this.name = 'parent'
    this.age = age
  }
  Parent.prototype.say = function () {
    console.log("Hello Word")
  }

  function Child(age) {
    Parent.call(this, age)  //继承Parent的属性值,注意:属性名相同情况下,遵循后者覆盖前者的规则
    this.type = 'child'
  }

  console.log(new Parent())    //Parent {name: 'parent', age: undefined}
  console.log(new Child(18))   //Child {name: 'parent', age: 18, type: 'child'}
</script>

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法

第二种: 借助原型链

javascript 复制代码
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

function Child() {
  this.type = 'child'
}

Child.prototype = new Parent()
console.log(new Child())  //Parent { type: 'child' }

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

javascript 复制代码
<script>
  function Parent() {
    this.name = 'parent';
    this.play = [1, 2, 3]
  }

  function Child() {
    this.type = 'child';
  }

  Child.prototype = new Parent();

  var s1 = new Child();
  var s2 = new Child();

  s1.play.push(4);
  console.log(s1, s1.play); 
  console.log(s2, s2.play); 
</script>

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。 那么还有更好的方式么?

第三种: 将前两种组合

javascript 复制代码
<script>
  function Parent() {
    this.name = 'parent';
    this.play = [1, 2, 3]
  }

  function Child() {
    Parent.call(this)
    this.type = 'child';
  }

  Child.prototype = new Parent();

  var s1 = new Child();
  var s2 = new Child();

  s1.play.push(4);
  console.log(s1, s1.play);
  console.log(s2, s2.play);
</script>

之前的问题都得以解决。但是这里又徒增了一个新问题 那就是 Parent3 的构造函数会多执行了一次 Child3.prototype = new Parent3() 这是我们不愿看到的。那么如何解决这个问题?

第四种: 组合继承的优化1

javascript 复制代码
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

function Child() {
  Parent.call(this)
  this.type = 'child'
}

Child.prototype = Parent.prototype

var s3 = new Child()
var s4 = new Child()
console.log(s3)  //Parent { name: 'parent', play: [ 1, 2, 3 ], type: 'child' }

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问 。 :::warning 但是我们来测试的时候发现子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4 :::

第五种:(最推荐使用): 寄生组合继承的优化2

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承

javascript 复制代码
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
Parent.prototype.say = function () {
  console.log("Hello Word")
}


function Child() {
  Parent.call(this)
  this.name = 'child'
}

//- Child.prototype = new Parent()                // 每次都需要new Parent(),带来开销,不推荐
Child.prototype = Parent.prototype                // 继承原型链
Child.prototype.constructor = Child               // 更改为子类的构造器

var child = new Child()

// child.__proto__ = Parent.prototype;            // 继承原型链
// Object.setPrototypeOf(child, Parent.prototype) // 继承原型链
// child.__proto__.constructor = Child            // 更改为子类的构造器


child.say() //Hello Word
console.log(child, child.__proto__)  // Child { name: 'child', play: [ 1, 2, 3 ] } Child { say: [Function] }
console.log(child.constructor, child.__proto__.constructor)  // [Function: Child] [Function: Child]
console.log(Object.getPrototypeOf(child))  // Child { say: [Function] }
console.log(Object.getPrototypeOf(child.__proto__)) //{}

寄生式组合继承写法上和组合继承基本类似,区别是如下这里:

diff 复制代码
- Dog.prototype = new Parent()
- Child.prototype.constructor = Child

+ function F() {}
+ F.prototype = Parent.prototype
+ let f = new F()
+ f.constructor = Child
+ Child.prototype = f
javascript 复制代码
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
Parent.prototype.say = function () {
  console.log("Hello Word")
}


function Child() {
  Parent.call(this)
  this.name = 'child'
}


// -Child.prototype = Parent.prototype
// -Child.prototype.constructor = Child
function F() {}
F.prototype = Parent.prototype
let f = new F()
f.constructor = Child
Child.prototype = f


var child = new Child() 
child.say() //Hello Word
console.log(child, child.__proto__)  // Child {name: "child", play: Array(3)} Parent {constructor: ƒ}
console.log(child.constructor, child.__proto__.constructor)  // [Function: Child] [Function: Child]
console.log(Object.getPrototypeOf(child))  //Child { constructor: [Function: Child] } 
console.log(Object.getPrototypeOf(child.__proto__)) // Parent { say: [Function] }
javascript 复制代码
function F() { }
F.prototype = Parent.prototype
let f = new F()
f.constructor = Child
Child.prototype = f

//-------------------- 对上面👆代码稍微封装后的代码 --------------------

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function inheritPrototype(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Child, Parent)

//------------------------------------------------------------------

var inherit = (function (c, p) {
  var F = function () { }
  return function (c, p) {
    F.prototype = p.prototype
    let f  = new F()
  	f.constructor = c
    c.prototype = f
  }
})()

//------------------------------------------------------------------
//如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
javascript 复制代码
var _inherit = (function () {
  var F = function () { };
  return function (c, p) {
    c.uber = p.prototype;
    F.prototype = p.prototype;
    c.prototype = new F();
    c.prototype.constructor = c;
  }
})();

function Person() { this.name = "person" }
Person.prototype.say = function () {
  console.log("Hello Word," + this.name)
}

function Student() { Person.call(this); this.name = "一缕清风" }
_inherit(Student, Person)
let superSay = Student.prototype.say
Student.prototype.say = function () {
  console.time("耗时")
  superSay.call(this)
  console.timeEnd("耗时")
}


let s = new Student()
s.say()

第六种:借助 Object.create + Object.getPrototypeOf

javascript 复制代码
function Button() {
  this.color = 'red';
}

var button = new Button();
Object.defineProperty(button, 'colorGet', {
  // enumerable默认为false。设为可枚举,不然 Object.create | Object.assign 方法会过滤该属性
  enumerable: true,
  get() {
    return "Could it return " + this.color
  }
});

var circleButton = Object.create(Object.getPrototypeOf(button), Object.getOwnPropertyDescriptors(button));
console.log(circleButton)

ES6的extends被编译后的JavaScript代码 ES6的代码最后都是要在浏览器上能够跑起来的,这中间就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。 那最后编译成了什么样子呢?

javascript 复制代码
function _possibleConstructorReturn(self, call) {
  // ...
  return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}

function _inherits(subClass, superClass) {
  // ...
  //看到没有
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}


var Parent = function Parent() {
  // 验证是否是 Parent 构造出来的 this
  _classCallCheck(this, Parent);
};

var Child = (function (_Parent) {
  _inherits(Child, _Parent);
  function Child() {
    _classCallCheck(this, Child);

    return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
  }
  return Child;
}(Parent));

核心是_inherits函数,可以看到它采用的依然也是第五种方式 ------ 寄生组合继承方式,同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢? 答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。

追问: 面向对象的设计一定是好的设计吗?

不一定。从继承的角度说,这一设计是存在巨大隐患的

第七种:class 实现继承

javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
class Dog extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age
  }
}

从设计思想上谈谈继承本身的问题

假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。

javascript 复制代码
class Car {
  constructor(id) {
    this.id = id;
  }

  drive() {
    console.log("drive!");
  }

  music() {
    console.log("music!")
  }

  addOil() {
    console.log("addOil")
  }

}

class NewEnergyCar extends Car { };
const ncar = new NewEnergyCar();
ncar.addOil()

现在可以实现车的功能,并且以此去扩展不同的车。 但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。 如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承

当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复 ,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好

那如何来解决继承的诸多问题呢? 用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。 顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

javascript 复制代码
<script>
  function drive() { console.log("drive!") }
  function music() { console.log("music!") }
  function addOil() { console.log("addOil") }

  function compose(...args) {
    return function () {
      // args.reverse().reduce((previousValue, currentValue) => {
      return args.reduceRight((previousValue, currentValue) => {
        console.log(previousValue, arguments);
        currentValue.apply(this, previousValue)
      }, null)
    }
  }


  /*     
  function compose() {
    // var args = Array.from(arguments)  // 转为数组类型
    // var args = Array.prototype.slice.call(arguments) //转为数组类型
    arguments.__proto__ = Array.prototype;  //还是arguments类型,知识可以使用数组的方法
    var args = arguments
    return function () {
      args.reverse().reduce((previousValue, currentValue) => {
        console.log(previousValue, arguments);
        currentValue.apply(this, arguments)
      }, "")
    }
  }  
*/


  let car = compose(drive, music, addOil)
  let newEnergyCar = compose(drive, music)
  car()
</script>

代码干净,复用性也很好。这就是面向组合的设计方式

第三章: 谈谈你对JS中this的理解

其实JS中的this是一个非常简单的东西,只需要理解它的执行规则就OK 在这里不想像其他博客一样展示太多的代码例子弄得天花乱坠, 反而不易理解 call/apply/bind可以显式绑定, 这里就不说了 主要这些场隐式绑定的场景讨论:

  1. 全局上下文
  2. 直接调用函数
  3. 对象.方法的形式调用
  4. DOM事件绑定(特殊)
  5. new构造函数绑定
  6. 箭头函数

1. 全局上下文

全局上下文默认this指向window, 严格模式下指向 undefined

2. 直接调用函数

html 复制代码
<script>
  let obj = {
    a: function() {
      console.log(this);
    }
  }

  obj.a();
  /* 
{a: ƒ}a: ƒ ()[[Prototype]]: Object
*/

  let func = obj.a;
  func();  // Window
</script>

这种情况是直接调用。this相当于全局上下文的情况

3. 对象.方法的形式调用

还是刚刚的例子,我如果这样写:

javascript 复制代码
obj.a();

这就是 对象.方法() 的情况,this指向这个对象

4. DOM事件绑定

onclick和addEventerListener中 this 默认指向绑定事件的元素 IE比较奇异,使用attachEvent,里面的this默认指向window

5. new+构造函数

此时构造函数中的this指向实例对象

6. 箭头函数?

箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。比如:

javascript 复制代码
let obj = {
  a: function() {
    let do = () => {
      console.log(this);
    }
    do();
  }
}

obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj

优先级: new > call、apply、bind > 对象.方法 > 直接调用

第四章: 能不能模拟实现一个new的效果?

new操作符做了哪些事情?

  1. 创建一个javascript空对象 {}
  2. 将要实例化对象的原形链指向该对象原形
  3. 绑定该对象为this的指向
  4. 返回该对象

new 被调用后做了三件事情:

  1. 让实例可以访问到私有属性
  2. 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
  3. 如果构造函数返回的结果不是引用数据类型,返回创建的对象,反之,返回引用数据类型
javascript 复制代码
function newOperator(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw 'newOperator function the first param must be a function'
  }

  // function Fn() { }
  // Fn.prototype = ctor.prototype
  // let obj = new Fn() 
  let obj = Object.create(ctor.prototype)
  let res = ctor.apply(obj, args)
  
  let isObject = res !== null && typeof res === 'object'
  let isFunction = typeof res === 'function'
  return isObject || isFunction ? res : obj
};



function Person(name, age) {
  this.name = name
  this.age = age
}

function fn(name, age) {
  return name + ' ' + age
}

// console.log(new Person("一缕清风", 20)); //Person { name: '一缕清风', age: 20 }
console.log(newOperator(Person, "一缕清风", 20)); //Person { name: '一缕清风', age: 20 }
console.log(newOperator(fn, "一缕清风", 20)); // fn {}
javascript 复制代码
function Person() { this.name = "一缕清风" }
function newOperator(ctor) {
	var obj = Object(null)
	obj.__proto__ = ctor.prototype
	let res = ctor.call(obj)
	let isObject = res !== null && typeof res === 'object'
	let isFunction = typeof res === 'function'
	return isObject || isFunction ? res : obj
}

console.log(newOperator(Person));

new 一个构造函数,如果函数有返回值,会发生什么情况?

:::tips 如果函数返回一个对象,那么new 调用这个函数的返回对象,否则返回 new 创建的新对象。 以下是函数返回 return {} 、 return null , return 1 , return true 等等的情况: :::

javascript 复制代码
function Fn1() { return undefined }
console.log(new Fn1());  // Fn1 {}

function Fn2() { return null }
console.log(new Fn2());  // Fn2 {}


function Fn3() { return 1 }
console.log(new Fn3());  //Fn3 {}

function Fn4() { return true }
console.log(new Fn4());  //Fn4 {}



function Fn5() { return {} }
console.log(new Fn5());  //{}

function Fn6() { return [] }
console.log(new Fn6());  // []



function Fn7() { return Array }
console.log(new Fn7());  // [Function: Array]
function Fn8() { return Object }
console.log(new Fn8());  // [Function: Object]
function Fn9() { return Boolean }
console.log(new Fn9());  // [Function: Boolean]



function Fn10() { return Boolean(true) }
console.log(new Fn10());  // Fn10 {}
function Fn11() { return String(true) }
console.log(new Fn11());  // Fn11 {}

第五章: 能不能模拟实现一个 bind 的效果?

实现bind之前,我们首先要知道它做了哪些事情

  1. 对于普通函数,绑定this指向
  2. 对于构造函数,要保证原函数的原型对象上的属性不能丢失
javascript 复制代码
<script>
  Function.prototype.bindDiy = function (context, ...args) {
    // // 异常处理
    // if (typeof this !== "function") {
    //   throw new Error("Function.prototype.bindDiy - what is trying to be bound is not callable")
    // }
    var self = this  // 保存this的值,它代表调用 bindDiy 的函数
    var bound = function () {
      console.log(this, "this")
      console.log(self, "self")
      console.log(context, "context")

      //这就话的意思是:如果new的话就使用this,函数调用的话就用context
      // var isUseNew = this instanceof self // ✅
      var isUseNew = Object.getPrototypeOf(this) === bound.prototype
      console.log(isUseNew, "是否使用new出对象")
      self.apply(isUseNew ? this : context,
        [...args.concat(Array.prototype.slice.call(arguments)),
          // context
        ])
    }

    // var fNOP = function () { }
    // fNOP.prototype = this.prototype
    // bound.prototype = new fNOP()
    // bound.prototype = Object.create(this.prototype);
    Object.setPrototypeOf(bound.prototype, self.prototype)
    return bound
  }

  //------------------------------ 开始测试 ------------------------------
  function A() {
    console.log(this, "this这是初始函数")
    console.log(arguments, "初始函数arguments")
  }
  A.bindDiy({ name: "一缕清风" })()
  console.log(`--------------------------------------------------------------------------------`)
  function B(params) {
    console.log(this, "this这是初始函数")
    console.log(arguments, "初始函数arguments")
  } //{ name: '一缕清风' } 输出this
  B.prototype.say = function () { console.log(this, this.name + ":说话"); }
  var BYD = B.bindDiy({ name: "一缕清风" }, 1, 2, 3, 4)
  var byd = new BYD()
  byd.say()   //说话
  console.log("自定义的new B.bindDiy()", byd);
  console.log(`--------------------------------------------------------------------------------`)
  console.log(`对比原生bind`)
  var BB = B.bind({ name: "一缕清风" }, 1, 2, 3, 4)
  var bb = new BB()
  bb.say()   //说话
  console.log("原生new B.bind()", bb);
</script>

也可以这么用 Object.create 来处理原型:

javascript 复制代码
<script>
  Function.prototype.bind = function (context, ...args) {
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var bound = function () {
      self.apply(this instanceof self ?
        this :
        context, args.concat(Array.prototype.slice.call(arguments)));
    }

    bound.prototype = Object.create(this.prototype);
    return bound;
  }
</script>

第六章: 能不能实现一个 call/apply 函数?

javascript 复制代码
<script>
  Function.prototype.call = function (obj) {
    let context = obj
    // let fn = Symbol('fn')
    // 保存this的值,它代表调用 bind 的函数
    context.fn = this
    console.log(this, "-----")
    let args = []
    for (let i = 1, len = arguments.length; i < len; i++) {
      args.push('arguments[' + i + ']')
    }
    let result = eval('context.fn(' + args + ')')
    delete context.fn
    return result
  }


  let obj = { name: "obj" };
  function func(x, y) {
    console.log(this)
    return x + y
  }
  console.log(func.call(obj, 10, 20))
</script>

不过我认为换成 ES6 的语法会更精炼一些:

javascript 复制代码
<script>
  Function.prototype.call = function (obj, ...args) {
    var context = obj || window;
    // let fn = Symbol('fn');
    // 保存this的值,它代表调用 bind 的函数
    context.fn = this;
    let result = eval('context.fn(...args)');
    delete context.fn
    return result;
  }

  let obj = { name: "obj" };
  function func(x, y) {
    console.log(this)
    return x + y
  }
  console.log(func.call(obj, 10, 20))
</script>

类似的,有apply的对应实现:

javascript 复制代码
Function.prototype.apply = function (obj, args) {
  let context = obj || window;
  context.fn = this;
  let result = eval('context.fn(...args)');
  delete context.fn
  return result;
}


let obj = { name: "obj" };
function func(x, y) {
  console.log(this)
  return x + y
}
console.log(func.apply(obj, [10, 20]))

第七章:实现 Object.create

javascript 复制代码
Object.create2 = function (proto, propertyObject = undefined) {
  if (typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object or null.')
    if (propertyObject == null) {
      new TypeError('Cannot convert undefined or null to object')
    }
    
    function F() { }
    F.prototype = proto
    const obj = new F()
    if (propertyObject != undefined) {
      Object.defineProperties(obj, propertyObject)
    }
    if (proto === null) {
      // 创建一个没有原型对象的对象,Object.create(null)
      obj.__proto__ = null
    }
    return obj
  }
}

第八章:实现 Object.assign

javascript 复制代码
Object.assign2 = function (target, ...source) {
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object')
  }
  
  let ret = Object(target)
  source.forEach(function (obj) {
    if (obj != null) {
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret[key] = obj[key]
        }
      }
    }
  })
  return ret
}

参考文献

:::info 类与对象篇(1) :::

相关推荐
CodeToGym4 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫5 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫9 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat10 分钟前
前端性能优化2
前端
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。2 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死3 小时前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试3 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana